<?php

/**
 * The definition for the contact class.
 *
 * @package CGContacts
 * @category Productivity
 * @author  calguy1000 <calguy1000@cmsmadesimple.org>
 * @copyright Copyright 2014 by Robert Campbell
 */

namespace CGContacts;

/**
 * A class to represent a category within the CGContacts module.
 *
 * @property int $id The category id.
 * @property string $name The category name.
 * @property string $description The category description
 * @property int $parent_id The id of this objects' parent.  or -1
 * @property int $item_order The order of this object amongst it's peers.
 * @property string $hierarchy (read only) The hierarchy of this object (path to the root) expressed in category ids.
 * @property string $long_name (read only) The hierarchy of this object (path to the root) expressed in terms of category names.
 */
class category implements \ArrayAccess
{
    /**
     * @ignore
     */
    private $_data = array('id'=>null,'name'=>null,'description'=>null,'parent_id'=>null,'item_order'=>null,'hierarchy'=>null,'long_name'=>null,'children'=>null);

    /**
     * @ignore
     */
    private $_dirty;

    /**
     * @ignore
     */
    private $_old_parent;

    /**
     * @ignore
     */
    private $_old_order;

    /**
     * @ignore
     */
    private static $_cached_categories;

    /**
     * @ignore
     */
    public function OffsetGet($key)
    {
        $key = strtolower(trim($key));
        switch( $key ) {
        case 'id':
        case 'name':
        case 'parent_id':
        case 'item_order':
        case 'hierarchy':
        case 'long_name':
            return $this->_data[$key];

        case 'desc':
        case 'description':
            return $this->_data['description'];

        case 'depth':
            if( isset($this->_data['depth']) ) return $this->_data['depth'];
            return count(explode('|',$this['long_name']))-1;

        case 'children':
            if( isset($this->_data['children']) ) return $this->_data['children'];
            break;

        default:
            throw new \CmsInvalidDataException($key.' is not a valid property of a '.__CLASS__.' object');
        }
    }

    /**
     * @ignore
     */
    public function OffsetSet($key,$val) {
        $key = strtolower(trim($key));
        switch( $key ) {
        case 'id':
        case 'hierarchy':
        case 'long_name':
            throw new \CmsInvalidDataException($key.' cannot be directly modified in a '.__CLASS__.' object');

        case 'item_order':
            $val = (int)$val;
            if( $val < 1 ) throw new \CmsInvalidDataException('item order must be a positive integer');
            if( !$this->_old_order ) $this->_old_order = $this->_data[$key];
            $this->_data[$key] = $val;
            $this->_dirty = TRUE;
            break;

        case 'parent_id':
            $val = (int)$val;
            if( $val == 0 || $val < -1 ) throw new \CmsInvalidDataException('parent_id must be a positive integer, or -1');
            // if the new parent is a child of the current item, disallow it.
            //if( self::is_child_of($val,$this['id']) ) throw new \CmsInvalidDataException('cannot move an item to be a descendant of itself');
            if( !$this->_old_order ) $this->_old_order = $this->_data['item_order'];
            if( !$this->_old_parent ) $this->_old_parent = $this->_data['parent_id'];
            $this->_data[$key] = $val;
            $this->_data['hierarchy'] = null;
            $this->_data['long_name'] = null;
            $this->_data['item_order'] = null;
            $this->_dirty = TRUE;
            break;

        case 'depth':
            $this->_data[$key] = max(0,(int)$val);
            break;

        case 'children':
            if( is_array($val) && count($val) ) {
                if( is_object($val[0]) ) $this->_data[$key] = $val;
            }
            break;

        case 'desc':
            $key = 'description';
        case 'description':
        case 'name':
            $val = trim($val);
            $this->_data[$key] = $val;
            $this->_dirty = TRUE;
            break;

        default:
            throw new \CmsInvalidDataException($key.' is not a valid property of a '.__CLASS__.' object');
        }
    }

    /**
     * @ignore
     */
    public function OffsetExists($key)
    {
        $key = strtolower(trim($key));
        switch( $key ) {
        case 'id':
        case 'hierarchy':
        case 'long_name':
        case 'item_order':
        case 'parent_id':
        case 'name':
        case 'desc':
        case 'description':
        case 'children':
            return true;

        default:
            throw new \CmsInvalidDataException($key.' is not a valid property of a '.__CLASS__.' object');
        }
    }

    /**
     * @ignore
     */
    public function OffsetUnset($key)
    {
        return;
    }

    /**
     * @ignore
     */
    protected function _validate_update()
    {
        if( $this['name'] == '' ) throw new \CmsInvalidDataException('Name cannot be empty in a '.__CLASS__.' object');

        // validate the parent_id
        if( $this['parent_id'] == '' || $this['parent_id'] == 0 || $this['parent_id'] < -1 ) $this['parent_id'] = -1;

        // validate name for the same parent
        $db = \cge_utils::get_db();
        $query = 'SELECT id FROM '.CGCONTACTS_CATS_TBL.' WHERE name = ? AND parent_id = ? AND id != ?';
        $tmp = (int) $db->GetOne($query,array($this['name'],$this['parent_id'],$this['id']));
        if( $tmp > 0 ) throw new \CmsInvalidDataException('A category with this name already exists for the same parent');
    }

    /**
     * @ignore
     */
    protected function _update()
    {
        if( !$this->_dirty ) return;
        $this->_validate_update();
        $db = \cge_utils::get_db();

        // if we've changed the parent id
        // we have to re-order all of the items that
        // used to be after this item with the same parent
        // similar to a delete + add

        // if we have no item order, we can create one
        if( $this['item_order'] < 1 ) {
            $query = 'SELECT COALESCE(MAX(item_order),0) + 1 FROM '.CGCONTACTS_CATS_TBL.' WHERE parent_id = ?';
            $this->_data['item_order'] = (int)$db->GetOne($query,array($this['parent_id']));
        }

        $query = 'UPDATE '.CGCONTACTS_CATS_TBL.' SET name = ?, description = ?, parent_id = ?, item_order = ? WHERE id = ?';
        $dbr = $db->Execute($query,array($this['name'],$this['description'],(int)$this['parent_id'],(int)$this['item_order'],(int)$this['id']));
        if( !$dbr ) throw new \Exception('SQL ERROR '.$db->ErrorMsg().' -- '.$db->sql);
        $this->_dirty = FALSE;
    }

    /**
     * @ignore
     */
    protected function _validate_insert()
    {
        // validate the name
        if( $this['name'] == '' ) throw new \CmsInvalidDataException('Name cannot be empty in a '.__CLASS__.' object');

        // validate the parent_id
        if( $this['parent_id'] == '' || $this['parent_id'] == 0 || $this['parent_id'] < -1 ) $this['parent_id'] = -1;

        // validate the name for the same parent
        $db = \cge_utils::get_db();
        $query = 'SELECT id FROM '.CGCONTACTS_CATS_TBL.' WHERE name = ? AND parent_id = ?';
        $tmp = (int) $db->GetOne($query,array($this['name'],$this['parent_id']));
        if( $tmp > 0 ) throw new \CmsInvalidDataException('A category with this name already exists for the same parent');
    }

    /**
     * @ignore
     */
    protected function _insert()
    {
        if( !$this->_dirty ) return;
        $this->_validate_insert();
        $db = \cge_utils::get_db();

        // if we have no item order, we can create one
        if( $this['item_order'] < 1 ) {
            $query = 'SELECT COALESCE(MAX(item_order),0) + 1 FROM '.CGCONTACTS_CATS_TBL.' WHERE parent_id = ?';
            $this->_data['item_order'] = (int)$db->GetOne($query,array($this['parent_id']));
        }

        // insert the record
        $query = 'INSERT INTO '.CGCONTACTS_CATS_TBL.' (name,description,parent_id,item_order) VALUES(?,?,?,?)';
        $dbr = $db->Execute($query,array($this['name'],$this['desc'],(int)$this['parent_id'],(int)$this['item_order']));
        if( !$dbr ) throw new \Exception('SQL ERROR '.$db->ErrorMsg().' -- '.$db->sql);

        // get the new id
        $this->_data['id'] = $db->Insert_ID();

        // mark not dirty
        $this->_dirty = FALSE;
    }

    /**
     * Test if category A is a child of category B
     *
     * @param int $a The first category id.
     * @param int $b The second category id.
     * @return bool
     */
    public static function is_child_of($a, $b)
    {
        // recursive function to test if a is a child of b
        // if b is -1 the result is always true
        // test if B is a child of A
        if( $a < 1 ) return FALSE;
        if( $b < 1 ) return TRUE; // everything is a child of the root somehow.
        $cats = self::get_all();

        if( !isset($cats[$a]) ) throw new \Exception('Could not find category with id '.$a);
        $cat = $cats[$a];
        if( $cat['parent_id'] == $b ) return TRUE;
        return self::is_child_of($cat['parent_id'],$b);
    }

    /**
     * Save this content object.
     *
     * @param bool $update_positions A flag indicating wether or not to update hierarchy positions across all categories.
     */
    public function save($update_positions = TRUE)
    {
        if( $this->_data['id'] > 0 ) {
            $this->_update();
        }
        else {
            $this->_insert();
        }

        if( $update_positions ) self::calculate_hierarchy_positions();
    }

    /**
     * Delete this category.
     *
     */
    public function delete()
    {
        if( $this->_data['id'] < 1 ) throw new \LogicException('Cannot delete a category that has not been saved');
        $db = \cge_utils::get_db();

        $db->BeginTrans();
        try {
            $sql = 'DELETE FROM '.CGCONTACTS_ADR_CATS_TBL.' WHERE cat_id = ?';
            $db->Execute($sql,array($this->_data['id']));

            $sql = 'DELETE FROM '.CGCONTACTS_CATS_TBL.' WHERE id = ?';
            $db->Execute($sql,array($this->_data['id']));

            $db->CommitTrans();
            $this->_dirty = true;
            $this->_data['id'] = null;
        }
        catch( \Exception $e ) {
            $db->RollbackTrans();
            throw $e;
        }
    }

    /**
     * @ignore
     */
    protected static function &_from_data($row)
    {
        $obj = new self();
        $obj->_data = $row;
        self::$_cached_categories[$obj['id']] = $obj;
        return $obj;
    }

    /**
     * @ignore
     */
    public function to_data()
    {
        $out = array();
        foreach( $this->_data as $key => $val ) {
            if( $key == 'children' ) {
                if( is_array($val) ) {
                    $children = array();
                    foreach( $val as $one ) {
                        $children[] = $one->to_data();
                    }
                    $out[$key] = $children;
                }
                continue;
            }

            $out[$key] = $val;
        }
        return $out;
    }

    /**
     * Load the specified category object, given it's id.
     *
     * @param int $id
     * @return category
     */
    public static function &load($id)
    {
        $id = (int)$id;
        if( $id < 1 ) throw new \CmsInvalidDataException("Invalid id passed to ".__METHOD__);

        $cats = self::get_all();
        if( isset($cats[$id]) ) return $cats[$id];

        // couldn't find it
        $obj = null;
        return $obj;
    }

    /**
     * Load all categories as a flat array.
     *
     * @return array
     */
    public static function get_all()
    {
        if( is_array(self::$_cached_categories) ) return self::$_cached_categories;

        self::$_cached_categories = array();
        $db = \cge_utils::get_db();
        $query = 'SELECT * FROM '.CGCONTACTS_CATS_TBL.' ORDER BY hierarchy';
        $tmp = $db->GetArray($query);
        if( is_array($tmp) && count($tmp) ) {
            foreach( $tmp as $row ) {
                self::_from_data($row);
            }
        }

        return self::$_cached_categories;
    }

    /**
     * Get the categories as a list
     * suitable for use in a dropdown.
     *
     * @return array
     */
    public static function get_list()
    {
        $tree = self::get_tree();
        if( !count($tree) ) return;
        $list = array();

        $flatten = function($tree,&$list,$depth = 0) use (&$flatten) {
            foreach( $tree as $node ) {
                $list[$node['id']] = str_repeat('&nbsp;&nbsp;',$depth).$node['name'];
                if( isset($node['children']) && count($node['children']) ) {
                    $flatten($node['children'],$list,$depth+1);
                }
            }
        };


        $flatten($tree,$list);
        return $list;
    }

    /**
     * Get the categories as a tree.
     *
     * @return array
     */
    public static function get_tree()
    {
        $list = self::get_all();
        if( !is_array($list) || count($list) == 0 ) return;

        // convert the flat list into a tree.
        // assume it's sorted by hierarchy.

        $get_tree = function($parent_id = -1, $depth = 0) use ($list,&$data,&$get_tree) {
            $out = array();
            foreach( $list as $one ) {
                if( $one['parent_id'] != $parent_id ) continue;

                $tmp = $get_tree($one['id'],$depth+1);
                if( is_array($tmp) && count($tmp) ) $one['children'] = $tmp;
                $one['depth'] = $depth; // will prolly fail.
                $out[] = $one;
            }
            return $out;
        };

        $tree = $get_tree();
        return $tree;
    }

    /**
     * Given a category name, attempt to find an id.
     *
     * @param string $str
     * @return int
     */
    public static function get_category_id($str)
    {
        $cats = self::get_all();
        if( !is_array($cats) || count($cats) == 0 ) return;

        $str = trim($str);
        if( (int) $str > 0 ) {
            // it's already an int
            $id = (int)$str;
            if( isset($cats[$id]) ) return $id;
        }
        else {
            $str = trim($str);
            foreach( $cats as $catid => $cat ) {
                if( strpos($cat['long_name'],$str) !== FALSE ) {
                    return $cat['id'];
                }
            }
        }
    }

    /**
     * Given a category id, get it's name
     *
     * @param int $id
     * @return string|null
     */
    public static function get_category_name($id)
    {
        $cats = self::get_all();
        if( !is_array($cats) || count($cats) == 0 ) return;
        $id = (int) $id;
        if( $id < 1 ) return;

        foreach( $cats as $catid => $cat ) {
            if( $cat['id'] == $id ) {
                return $cat['name'];
            }
        }
    }

    /**
     * @internal
     * @ignore
     */
    public static function calculate_hierarchy_positions()
    {
        $db = \cge_utils::get_db();

        $query = "SELECT id, item_order, name FROM ".CGCONTACTS_CATS_TBL;
        $dbresult = $db->Execute($query);
        if( !$dbresult ) throw new \CmsException('SQL Error: '.$db->sql.' -- '.$db->ErrorMsg());
        while ($dbresult && $row = $dbresult->FetchRow()) {
            $current_hierarchy_position = "";
            $current_long_name = "";
            $content_id = $row['id'];
            $current_parent_id = $row['id'];
            $count = 0;

            while ($current_parent_id > 0) {
                $query = "SELECT id, item_order, name, parent_id FROM ".CGCONTACTS_CATS_TBL." WHERE id = ?";
                $row2 = $db->GetRow($query, array($current_parent_id));
                if ($row2) {
                    $current_hierarchy_position = str_pad($row2['item_order'], 5, '0', STR_PAD_LEFT) . "." . $current_hierarchy_position;
                    $current_long_name = $row2['name'] . ' | ' . $current_long_name;
                    $current_parent_id = $row2['parent_id'];
                    $count++;
                }
                else {
                    $current_parent_id = -1;
                }
            }

            if (strlen($current_hierarchy_position) > 0) {
                $current_hierarchy_position = substr($current_hierarchy_position, 0, strlen($current_hierarchy_position) - 1);
            }

            if (strlen($current_long_name) > 0) $current_long_name = substr($current_long_name, 0, strlen($current_long_name) - 3);

            $query = "UPDATE ".CGCONTACTS_CATS_TBL." SET hierarchy = ?, long_name = ? WHERE id = ?";
            $dbr = $db->Execute($query, array($current_hierarchy_position, $current_long_name, $content_id));
            if( !$dbr ) die($db->sql.' -- '.$db->ErrorMsg());
        }
    }

    /**
     * Expand a category id into it and its children.
     *
     * @internal
     * @ignore
     */
    public static function expand_category($catid)
    {
        $catid = (int) $catid;
        $db = \cge_utils::get_db();
        $out = null;
        if( $catid < 1 ) {
            // all categories
            $sql = 'SELECT id FROM '.CGCONTACTS_CATS_TBL.' ORDER BY hierarchy';
            $out = $db->GetCol();
        }
        else {
            $cat = self::load($catid);
            if( $cat ) {
                $sql = 'SELECT id FROM '.CGCONTACTS_CATS_TBL.' WHERE hierarchy LIKE ?';
                $out = $db->GetCol($sql,array($cat['hierarchy'].'%'));
            }
        }
        return $out;
    }
} // end of class

?>
