<?php
/* For licensing terms, see /license.txt */
/**
 * @TODO: Improve description
 * @package chamilo.hookmanagement
 */
class HookManagement implements HookManagementInterface
{
    /**
     * Constructor
     */
    protected function __construct()
    {
        $this->tables[TABLE_HOOK_OBSERVER] = Database::get_main_table(TABLE_HOOK_OBSERVER);
        $this->tables[TABLE_HOOK_EVENT] = Database::get_main_table(TABLE_HOOK_EVENT);
        $this->tables[TABLE_HOOK_CALL] = Database::get_main_table(TABLE_HOOK_CALL);

        $this->hookCalls = $this->listAllHookCalls();
        $this->hookEvents = $this->listAllHookEvents();
        $this->hookObservers = $this->listAllHookObservers();
    }

    /**
     * Instance the hook manager
     * @staticvar null $result
     * @return HookManagement
     */
    public static function create()
    {
        static $result = null;

        return $result ? $result : $result = new self();
    }

    /**
     * Insert hook into Database. Return insert id
     * @param string $eventName
     * @param string $observerClassName
     * @param int $type
     *
     * @return int
     */
    public function insertHook($eventName, $observerClassName, $type)
    {
        if ($type === HOOK_EVENT_TYPE_ALL) {
            $this->insertHook($eventName, $observerClassName, HOOK_EVENT_TYPE_PRE);
            $this->insertHook($eventName, $observerClassName, HOOK_EVENT_TYPE_POST);
        } else {
            $this->insertHookIfNotExist($eventName, $observerClassName);
            // Check if exists hook call
            $row = Database::select('id, enabled',
                $this->tables[TABLE_HOOK_CALL],
                array(
                    'where' => array(
                        'hook_event_id = ? ' => $this->hookEvents[$eventName],
                        'AND hook_observer_id = ? ' => $this->hookObservers[$observerClassName],
                        'AND type = ? ' => $type,
                    ),
                ),
                'ASSOC');

            if (!empty($row) && is_array($row)) {
                // Check if is hook call is active
                if ((int) $row['enabled'] === 0) {
                    Database::update(
                        $this->tables[TABLE_HOOK_CALL],
                        array(
                            'enabled' => 1,
                        ),
                        array(
                            'id = ?' => $row['id'],
                        )
                    );
                }
            }
        }

    }

    /**
     * Delete hook from Database. Return deleted rows number
     * @param string $eventName
     * @param string $observerClassName
     * @param int $type
     *
     * @return int
     */
    public function deleteHook($eventName, $observerClassName, $type)
    {
        if ($type === HOOK_EVENT_TYPE_ALL) {
            $this->deleteHook($eventName, $observerClassName, HOOK_EVENT_TYPE_PRE);
            $this->deleteHook($eventName, $observerClassName, HOOK_EVENT_TYPE_POST);
        } else {
            $this->insertHookIfNotExist($eventName, $observerClassName);

            Database::update(
                $this->tables[TABLE_HOOK_CALL],
                array(
                    'enabled' => 0,
                ),
                array(
                    'id = ? ' => $this->hookCalls[$eventName][$observerClassName][$type],
                )
            );
        }
    }

    /**
     * Update hook observer order by hook event
     * @param $eventName
     * @param $type
     * @param $hookOrders
     *
     * @return int
     */
    public function orderHook($eventName, $type, $hookOrders)
    {
        foreach ($this->hookCalls[$eventName] as $observerClassName => $types) {
            foreach ($hookOrders as $oldOrder => $newOrder)
            {
                $res = Database::update(
                    $this->tables[TABLE_HOOK_CALL],
                    array(
                        'hook_order ' => $newOrder,
                    ),
                    array(
                        'id = ? ' => $types[$type],
                        'AND hook_order = ? ' => $oldOrder,
                    )
                );

                if ($res) {
                    break;
                }
            }
        }
    }

    /**
     * Return a list an associative array where keys are the active hook observer class name
     * @param $eventName
     *
     * @return array
     */
    public function listHookObservers($eventName)
    {
        $array = array();
        $joinTable = $this->tables[TABLE_HOOK_CALL] . ' hc' .
            ' INNER JOIN ' . $this->tables[TABLE_HOOK_EVENT] . ' he' .
            ' ON hc.hook_event_id = he.id ' .
            ' INNER JOIN ' . $this->tables[TABLE_HOOK_OBSERVER] . ' ho ' .
            ' ON hc.hook_observer_id = ho.id ';
        $columns = 'ho.class_name, ho.path, ho.plugin_name, hc.enabled';
        $where = array('where' => array('he.class_name = ? ' => $eventName, 'AND hc.enabled = ? ' => 1));
        $rows = Database::select($columns, $joinTable, $where);

        foreach ($rows as $row) {
            $array[$row['class_name']] = $row;
        }

        return $array;
    }

    /**
     * Return a list an associative array where keys are the active hook observer class name
     * @return array
     */
    public function listAllHookObservers()
    {
        $array = array();
        $columns = 'id, class_name';
        $rows = Database::select($columns, $this->tables[TABLE_HOOK_OBSERVER]);

        foreach ($rows as $row) {
            $array[$row['class_name']] = $row['id'];
        }

        return $array;
    }

    /**
     * Return a list an associative array where keys are the active hook observer class name
     * @return array
     */
    public function listAllHookEvents()
    {
        $array = array();
        $columns = 'id, class_name';
        $rows = Database::select($columns, $this->tables[TABLE_HOOK_EVENT]);

        foreach ($rows as $row) {
            $array[$row['class_name']] = $row['id'];
        }

        return $array;
    }

    /**
     * Return a list an associative array where keys are the active hook observer class name
     * @return array
     */
    public function listAllHookCalls()
    {
        $array = array();
        $joinTable = $this->tables[TABLE_HOOK_CALL] . ' hc' .
            ' INNER JOIN ' . $this->tables[TABLE_HOOK_EVENT] . ' he' .
            ' ON hc.hook_event_id = he.id ' .
            ' INNER JOIN ' . $this->tables[TABLE_HOOK_OBSERVER] . ' ho ' .
            ' ON hc.hook_observer_id = ho.id ';
        $columns = 'he.class_name AS event_class_name, ho.class_name AS observer_class_name, hc.id AS id, hc.type AS type';
        $rows = Database::select($columns, $joinTable);

        foreach ($rows as $row) {
            $array[$row['event_class_name']][$row['observer_class_name']][$row['type']] = $row['id'];
        }

        return $array;
    }

    /**
     * Check if hooks (event, observer, call) exist in Database, if not,
     * Will insert them into their respective table
     * @param string $eventName
     * @param string $observerClassName
     *
     * @return int
     */
    public function insertHookIfNotExist($eventName = null, $observerClassName = null)
    {
        // Check if exists hook event
        if (isset($eventName) && !isset($this->hookEvents[$eventName])) {
            $attributes = array(
                'class_name' => $eventName,
                'description' => get_lang('HookDescription' . $eventName),
            );
            $id = Database::insert($this->tables[TABLE_HOOK_EVENT], $attributes);
            $this->hookEvents[$eventName] = $id;
        }

        // Check if exists hook observer
        if (isset($observerClassName) &&
            !isset($this->hookObservers[$observerClassName])
        ){
            $object = $observerClassName::create();
            $attributes = array(
                'class_name' => $observerClassName,
                'path' => $object->getPath(),
                'plugin_name' => $object->getPluginName(),
            );
            $id = Database::insert($this->tables[TABLE_HOOK_OBSERVER], $attributes);
            $this->hookObservers[$observerClassName] = $id;
        }

        if (
            isset($eventName) &&
            isset($observerClassName) &&
            !isset($this->hookCalls[$eventName][$observerClassName])
        ) {
            // HOOK TYPE PRE

            $row = Database::select(
                'MAX(hook_order) as hook_order',
                $this->tables[TABLE_HOOK_CALL],
                array(
                    'where' => array(
                        'hook_event_id = ? ' =>$this->hookEvents[$eventName],
                        'AND type = ? ' => HOOK_EVENT_TYPE_PRE,
                    ),
                ),
                'ASSOC'
            );

            // Check if exists hook call
            $id = Database::insert(
                $this->tables[TABLE_HOOK_CALL],
                array(
                    'hook_event_id' => $this->hookEvents[$eventName],
                    'hook_observer_id' => $this->hookObservers[$observerClassName],
                    'type' => HOOK_EVENT_TYPE_PRE,
                    'enabled' => 0,
                    'hook_order' => $row['hook_order'] + 1,
                )
            );

            $this->hookCalls[$eventName][$observerClassName][HOOK_EVENT_TYPE_PRE] = $id;

            // HOOK TYPE POST

            $row = Database::select(
                'MAX(hook_order) as hook_order',
                $this->tables[TABLE_HOOK_CALL],
                array(
                    'where' => array(
                        'hook_event_id = ? ' =>$this->hookEvents[$eventName],
                        'AND type = ? ' => HOOK_EVENT_TYPE_POST,
                    ),
                ),
                'ASSOC'
            );

            // Check if exists hook call
            $id = Database::insert(
                $this->tables[TABLE_HOOK_CALL],
                array(
                    'hook_event_id' => $this->hookEvents[$eventName],
                    'hook_observer_id' => $this->hookObservers[$observerClassName],
                    'type' => HOOK_EVENT_TYPE_POST,
                    'enabled' => 0,
                    'hook_order' => $row['hook_order'] + 1,
                )
            );

            $this->hookCalls[$eventName][$observerClassName][HOOK_EVENT_TYPE_POST] = $id;

        } elseif (isset($eventName) && !isset($observerClassName)) {
            foreach ($this->hookObservers as $observer => $id) {
                $this->insertHookIfNotExist($eventName, $observer);
            }
        } elseif (!isset($eventName) && isset($observerClassName)) {
            foreach ($this->hookEvents as $event => $id) {
                $this->insertHookIfNotExist($event, $observerClassName);
            }
        }

        return 1;
    }

    /**
     * Return the hook call id identified by hook event, hook observer and type
     * @param string $eventName
     * @param string $observerClassName
     * @param int $type
     *
     * @return mixed
     */
    public function getHookCallId($eventName, $observerClassName, $type)
    {
        $eventName = Database::escape_string($eventName);
        $observerClassName($observerClassName);
        $type = Database::escape_string($type);
        $joinTable = $this->tables[TABLE_HOOK_CALL] . ' hc' .
            ' INNER JOIN ' . $this->tables[TABLE_HOOK_EVENT] . ' he' .
            ' ON hc.hook_event_id = he.id ' .
            ' INNER JOIN ' . $this->tables[TABLE_HOOK_OBSERVER] . ' ho ' .
            ' ON hc.hook_observer_id = ho.id ';
        $row = Database::select(
            'id',
            $joinTable,
            array(
                'where' => array(
                    'he.class_name = ? ' => $eventName,
                    'AND ho.class_name = ? ' => $observerClassName,
                    'AND hc.type = ? ' => $type,
                ),
            ),
            'ASSOC'
        );

        return $row['id'];
    }
}
