<?php

namespace Icinga\Module\Director\Objects;

use Countable;
use Exception;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Db\Cache\PrefetchCache;
use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
use Iterator;
use RuntimeException;

class IcingaObjectGroups implements Iterator, Countable, IcingaConfigRenderer
{
    protected $storedGroups = array();

    protected $groups = array();

    protected $modified = false;

    protected $object;

    private $position = 0;

    protected $idx = array();

    public function __construct(IcingaObject $object)
    {
        $this->object = $object;

        if (! $object->hasBeenLoadedFromDb() && PrefetchCache::shouldBeUsed()) {
            /** @var IcingaObjectGroup $class */
            $class = $this->getGroupClass();
            $class::prefetchAll($this->object->getConnection());
        }
    }

    #[\ReturnTypeWillChange]
    public function count()
    {
        return count($this->groups);
    }

    #[\ReturnTypeWillChange]
    public function rewind()
    {
        $this->position = 0;
    }

    public function hasBeenModified()
    {
        return $this->modified;
    }

    #[\ReturnTypeWillChange]
    public function current()
    {
        if (! $this->valid()) {
            return null;
        }

        return $this->groups[$this->idx[$this->position]];
    }

    #[\ReturnTypeWillChange]
    public function key()
    {
        return $this->idx[$this->position];
    }

    #[\ReturnTypeWillChange]
    public function next()
    {
        ++$this->position;
    }

    #[\ReturnTypeWillChange]
    public function valid()
    {
        return array_key_exists($this->position, $this->idx);
    }

    public function get($key)
    {
        if (array_key_exists($key, $this->groups)) {
            return $this->groups[$key];
        }

        return null;
    }

    /**
     * @param $group
     * @return $this
     * @throws NotFoundError
     */
    public function set($group)
    {
        if (! is_array($group)) {
            $group = array($group);
        }

        $existing = array_keys($this->groups);
        $new = array();
        $class = $this->getGroupClass();
        $unset = array();

        foreach ($group as $k => $g) {
            if ($g instanceof $class) {
                $new[] = $g->object_name;
            } else {
                if (empty($g)) {
                    $unset[] = $k;
                    continue;
                }

                $new[] = $g;
            }
        }

        foreach ($unset as $k) {
            unset($group[$k]);
        }

        sort($existing);
        sort($new);
        if ($existing === $new) {
            return $this;
        }

        $this->groups = array();
        if (empty($group)) {
            $this->modified = true;
            $this->refreshIndex();
            return $this;
        }

        return $this->add($group);
    }

    /**
     * Magic isset check
     *
     * @return boolean
     */
    public function __isset($group)
    {
        return array_key_exists($group, $this->groups);
    }

    public function remove($group)
    {
        if (array_key_exists($group, $this->groups)) {
            unset($this->groups[$group]);
        }

        $this->modified = true;
        $this->refreshIndex();
    }

    protected function refreshIndex()
    {
        ksort($this->groups);
        $this->idx = array_keys($this->groups);
    }

    /**
     * @param $group
     * @param string $onError
     * @return $this
     * @throws NotFoundError
     * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
     */
    public function add($group, $onError = 'fail')
    {
        // TODO: only one query when adding array
        if (is_array($group)) {
            foreach ($group as $g) {
                $this->add($g, $onError);
            }
            return $this;
        }

        if (is_int($group)) {
            $group = (string) $group;
        }

        /** @var IcingaObjectGroup $class */
        $class = $this->getGroupClass();

        if ($group instanceof $class) {
            if (array_key_exists($group->getObjectName(), $this->groups)) {
                return $this;
            }

            $this->groups[$group->object_name] = $group;
        } elseif (is_string($group)) {
            if (array_key_exists($group, $this->groups)) {
                return $this;
            }

            $connection = $this->object->getConnection();

            try {
                $this->groups[$group] = $class::load($group, $connection);
            } catch (NotFoundError $e) {
                switch ($onError) {
                    case 'autocreate':
                        $newGroup = $class::create(array(
                            'object_type' => 'object',
                            'object_name' => $group
                        ));
                        $newGroup->store($connection);
                        $this->groups[$group] = $newGroup;
                        break;
                    case 'fail':
                        throw new NotFoundError(
                            'The group "%s" doesn\'t exist.',
                            $group
                        );
                        break;
                    case 'ignore':
                        return $this;
                }
            }
        } else {
            throw new RuntimeException(
                'Invalid group object: %s',
                var_export($group, true)
            );
        }

        $this->modified = true;
        $this->refreshIndex();

        return $this;
    }

    protected function getGroupTableName()
    {
        return $this->object->getTableName() . 'group';
    }


    protected function getGroupMemberTableName()
    {
        return $this->object->getTableName() . 'group_' . $this->getType();
    }

    public function listGroupNames()
    {
        return array_keys($this->groups);
    }

    public function listOriginalGroupNames()
    {
        return array_keys($this->storedGroups);
    }

    public function getType()
    {
        return $this->object->getShortTableName();
    }

    protected function loadFromDb()
    {
        $db = $this->object->getDb();
        $connection = $this->object->getConnection();

        $type = $this->getType();

        $table = $this->object->getTableName();
        $query = $db->select()->from(
            array('go' => $table . 'group_' . $type),
            array()
        )->join(
            array('g' => $table . 'group'),
            'go.' . $type . 'group_id = g.id',
            '*'
        )->where('go.' . $type . '_id = ?', $this->object->id)
        ->order('g.object_name');

        $class = $this->getGroupClass();
        $this->groups = $class::loadAll($connection, $query, 'object_name');
        $this->setBeingLoadedFromDb();

        return $this;
    }

    public function store()
    {
        $storedGroups = array_keys($this->storedGroups);
        $groups = array_keys($this->groups);

        $objectId = $this->object->id;
        $type = $this->getType();

        $objectCol = $type . '_id';
        $groupCol = $type . 'group_id';

        $toDelete = array_diff($storedGroups, $groups);
        foreach ($toDelete as $group) {
            $where = sprintf(
                $objectCol . ' = %d AND ' . $groupCol . ' = %d',
                $objectId,
                $this->storedGroups[$group]->id
            );

            $this->object->db->delete(
                $this->getGroupMemberTableName(),
                $where
            );
        }

        $toAdd = array_diff($groups, $storedGroups);
        foreach ($toAdd as $group) {
            $this->object->db->insert(
                $this->getGroupMemberTableName(),
                array(
                    $objectCol => $objectId,
                    $groupCol => $this->groups[$group]->id
                )
            );
        }
        $this->setBeingLoadedFromDb();

        return true;
    }

    public function setBeingLoadedFromDb()
    {
        $this->storedGroups = array();
        foreach ($this->groups as $k => $v) {
            $this->storedGroups[$k] = clone($v);
            $this->storedGroups[$k]->id = $v->id;
        }

        $this->modified = false;
    }

    protected function getGroupClass()
    {
        return __NAMESPACE__ . '\\Icinga' .ucfirst($this->object->getShortTableName()) . 'Group';
    }

    public static function loadForStoredObject(IcingaObject $object)
    {
        $groups = new static($object);

        if (PrefetchCache::shouldBeUsed()) {
            $groups->groups = PrefetchCache::instance()->groups($object);
            $groups->setBeingLoadedFromDb();
        } else {
            $groups->loadFromDb();
        }

        return $groups;
    }

    public function toConfigString()
    {
        $groups = array_keys($this->groups);

        if (empty($groups)) {
            return '';
        }

        return c::renderKeyValue('groups', c::renderArray($groups));
    }

    public function toLegacyConfigString($additionalGroups = array())
    {
        $groups = array_merge(array_keys($this->groups), $additionalGroups);
        $groups = array_unique($groups);

        if (empty($groups)) {
            return '';
        }

        $type = $this->object->getLegacyObjectType();
        return c1::renderKeyValue($type.'groups', c1::renderArray($groups));
    }

    public function __toString()
    {
        try {
            return $this->toConfigString();
        } catch (Exception $e) {
            trigger_error($e);
            $previousHandler = set_exception_handler(
                function () {
                }
            );
            restore_error_handler();
            if ($previousHandler !== null) {
                call_user_func($previousHandler, $e);
                die();
            } else {
                die($e->getMessage());
            }
        }
    }

    public function __destruct()
    {
        unset($this->storedGroups);
        unset($this->groups);
        unset($this->object);
    }
}
