<?php
namespace Mapbender\DataSourceBundle\Element;

use Doctrine\DBAL\Connection;
use FOM\UserBundle\Entity\User;
use Mapbender\DataSourceBundle\Component\DataStoreService;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

/**
 * Class BaseElement
 */
abstract class BaseElement extends BaseElementLegacy
{
    /**
     * Prepare elements recursive.
     *
     * @param mixed[] $items
     * @return array
     */
    public function prepareItems($items)
    {
        if (!is_array($items)) {
            return $items;
        } elseif (self::isAssoc($items)) {
            $items = $this->prepareItem($items);
        } else {
            foreach ($items as $key => $item) {
                $items[ $key ] = $this->prepareItem($item);
            }
        }
        return $items;
    }

    /**
     * @param Request $request
     * @return Response
     * @deprecated do not rely on method name inflection magic; use your own implementation supporting valid actions explicitly
     */
    public function handleHttpRequest(Request $request)
    {
        // If a child class implements httpAction, but not handleHttpRequest, delegate
        // to that httpAction implementation
        $r = new \ReflectionMethod($this, 'httpAction');
        if ($r->getDeclaringClass()->name !== __CLASS__) {
            return $this->httpAction($request->attributes->get('action'));
        }
        return $this->handleHttpRequestMagically($request);
    }

    /**
     *
     * @inheritdoc
     * @deprecated
     */
    public function httpAction($action)
    {
        $request = $this->container->get('request_stack')->getCurrentRequest();
        return $this->handleHttpRequest($request);
    }

    /**
     * Prepare element by type
     *
     * @param mixed[] $item
     * @return mixed[]
     */
    protected function prepareItem($item)
    {
        if (!isset($item["type"])) {
            return $item;
        }

        if (isset($item["children"])) {
            $item["children"] = $this->prepareItems($item["children"]);
        }

        switch ($item['type']) {
            case 'select':
                return $this->prepareSelectItem($item);
            default:
                return $item;

        }
    }

    /**
     * Reformat single select item option for vis-ui consumption.
     * @param string $value
     * @param string $label
     * @param mixed[] $selectItem
     * @param array $allProperties will be placed in a 'properties' subarray
     * @return array
     * @todo: this is vis-ui specific and doesn't really belong here
     * @since 0.1.15
     */
    protected function formatSelectItemOption($value, $label, array $selectItem, array $allProperties = array())
    {
        $option = array(
            // Force object emission to bypass vis-ui's "isValuePack" path
            // use fancy underscores to reinforce key
            // order, in case json encoding / json parsing
            // performs any sorting
            '___value' => $value,
            '__label' => $label,
        );
        if ($allProperties) {
            // emit entire associative row array as well, minus nonserializable
            // resources (e.g. certain Oracle types)
            $option['properties'] = array_filter($allProperties, function($column) {
                return !is_resource($column);
            });
        }
        return $option;
    }

    /**
     * Reformat statically defined select item options for vis-ui
     * consumption.
     *
     * @param mixed[] $item
     * @return array
     * @since 0.1.15
     */
    protected function formatStaticSelectItemOptions($item)
    {
        if (empty($item['options'])) {
            return array();
        } elseif (!is_array($item['options'])) {
            throw new \RuntimeException("Invalid type " . gettype($item['options']) . " in select item options. Expected array. Item: " . print_r($item, true));
        } else {
            // bring options into same format as generated by
            // SQL path, so mix and match works.
            $options = array();
            foreach ($item['options'] as $value => $label) {
                $options[] = $this->formatSelectItemOption($value, $label, $item);
            }
            return $options;
        }
    }

    /**
     * @param mixed[] $item
     * @return mixed[]
     * @since 0.1.15
     */
    protected function prepareSelectItem($item)
    {
        $dsConfigKey = $this->getDataStoreKeyInFormItemConfig();
        $paths = array_flip(array(
            $dsConfigKey,
            'dataStore',
            'service',
            'sql',
        ));
        $configuredPaths = array_keys(array_intersect_key($paths, array_filter($item)));
        if (count($configuredPaths) > 1) {
            $message
                = 'Select item has option configurations for ' . implode(', ', $configuredPaths) . '.'
                . ' Executing only ' . $configuredPaths[0] . ' path.'
            ;
            // NOTE: E_USER_DEPRECATED is the only error level currently guaranteed to end up in logs
            @trigger_error("WARNING: {$message}", E_USER_DEPRECATED);
        }

        if (!empty($item[$dsConfigKey]) || !empty($item['dataStore'])) {
            return $this->prepareDataStoreSelectItem($item);
        } elseif (!empty($item['service'])) {
            return $this->prepareServiceSelectItem($item);
        } elseif (!empty($item['sql'])) {
            return $this->prepareSqlSelectItem($item);
        } else {
            $item['options'] = $this->formatStaticSelectItemOptions($item);
            return $item;
        }
    }

    /**
     * Reformat single select option loaded from 'sql' path for
     * vis-ui consumption.
     *
     * @param array $row
     * @param mixed[] $selectItem
     * @return mixed[]
     * @since 0.1.15
     */
    protected function formatSqlSelectItemOption($row, $selectItem)
    {
        // Legacy quirk: reset / end allows using a single-column
        // select where each option's submit value is the same as its
        // label.
        // When processing a multi-column row, the submit value
        // is taken from the first column, and the displayed label
        // from the _last_ column.
        $value = reset($row);
        $label = end($row);
        return $this->formatSelectItemOption($value, $label, $selectItem, $row);
    }

    /**
     * @param mixed[] $item
     * @return mixed[]
     * @since 0.1.15
     */
    protected function prepareSqlSelectItem($item)
    {
        $connectionName = isset($item['connection']) ? $item['connection'] : 'default';
        $sql = $item['sql'];
        $item['options'] = $this->formatStaticSelectItemOptions($item);

        unset($item['sql']);
        unset($item['connection']);
        $connection = $this->getDbalConnectionByName($connectionName);
        foreach ($connection->fetchAll($sql) as $row) {
            $item['options'][] = $this->formatSqlSelectItemOption($row, $item);
        }
        return $item;
    }

    /**
     * @param mixed[] $item
     * @return mixed[]
     * @since 0.1.15
     * @deprecated for being untestable and unmaintainable; override prepareSelectItem for
     *    project-specific customization
     */
    protected function prepareServiceSelectItem($item)
    {
        @trigger_error("Taking deprecated 'service' path to generate select item options. Extend prepareSelectItem to customize your project instead. Item: " . print_r($item, true), E_USER_DEPRECATED);
        $serviceInfo = $item['service'];
        if (empty($serviceInfo['serviceName'])) {
            throw new \RuntimeException("Invalid 'service' select item configuration, missing required serviceName. Item: " . print_r($item, true));
        }

        $serviceName = $serviceInfo['serviceName'];
        $method = isset($serviceInfo['method']) ? $serviceInfo['method'] : 'get';
        $args = isset($serviceInfo['args']) ? $item['args'] : '';
        $service = $this->container->get($serviceName);
        $options = $service->$method($args);

        $item['options'] = $options;
        return $item;
    }

    /**
     * @param mixed[] $item
     * @return mixed[]
     * @since 0.1.15
     */
    protected function prepareDataStoreSelectItem($item)
    {
        $dsConfigKey = $this->getDataStoreKeyInFormItemConfig();
        if (empty($item[$dsConfigKey])) {
            if (!empty($item['dataStore'])) {
                // fall back to using default 'dataStore' key
                $dsConfigKey = 'dataStore';
            }
        }
        $dataStoreInfo = $item[$dsConfigKey];
        $dataStore = $this->getDataStoreService()->get($dataStoreInfo['id']);
        $options = array();
        foreach ($dataStore->search() as $dataItem) {
            $options[$dataItem->getId()] = $dataItem->getAttribute($dataStoreInfo["text"]);
        }
        if (isset($dataStoreInfo[$dsConfigKey]['popupItems'])) {
            $item[$dsConfigKey]['popupItems'] = $this->prepareItems($item[$dsConfigKey]['popupItems']);
        }
        $item['options'] = $options;
        return $item;
    }

    /**
     * Override point for child classes
     *
     * @return DataStoreService
     * @since 0.1.15
     */
    protected function getDataStoreService()
    {
        /** @var DataStoreService $service */
        $service = $this->container->get('data.source');
        return $service;
    }

    /**
     * @param string $name
     * @return Connection
     * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
     * @since 0.1.15
     */
    protected function getDbalConnectionByName($name)
    {
        /** @var Connection $connection */
        $connection = $this->container->get("doctrine.dbal.{$name}_connection");
        return $connection;
    }

    /**
     * @return int|string
     * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
     * @throws \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException
     */
    protected function getUserId()
    {
        /** @var TokenStorageInterface $tokenStorage */
        $tokenStorage = $this->container->get('security.token_storage');
        $token = $tokenStorage->getToken();
        if ($token instanceof AnonymousToken) {
            return 0;
        }
        $user = $token->getUser();
        if (is_object($user) && $user instanceof User) {
            return $user->getId();
        } else {
            return $token->getUsername();
        }
    }

    /**
     * @param array $arr
     * @return bool
     */
    protected static function isAssoc($arr)
    {
        return array_keys($arr) !== range(0, count($arr) - 1);
    }

    public function getFrontendTemplateVars()
    {
        // The default fallback getConfiguration call (see below) can be outrageously expensive.
        // This can make a default inherited render() call very slow. BaseElement child classes
        // generally have pretty trivial templates, accessing only id and title of the Element
        // entity, so this is completely appropriate here.
        return $this->entity->getConfiguration();
    }

    public function getConfiguration()
    {
        $configuration = $this->entity->getConfiguration();
        if (isset($configuration['children'])) {
            $configuration['children'] = $this->prepareItems($configuration['children']);
        }
        return $configuration;
    }

    /**
     * Names the key inside the schema top-level config where data store config
     * is located.
     * Override support for child classes (Digitizer uses featureType instead
     * of the default dataStore).
     * @return string
     */
    protected function getDataStoreKeyInSchemaConfig()
    {
        return 'dataStore';
    }

    /**
     * Names the key inside a form item where settings for a referenced
     * data store are placed. This is an optional path inside select items.
     * Override support for child classes (Digitizer may or may not use featureType
     * instead of the default dataStore).
     * @return string
     */
    protected function getDataStoreKeyInFormItemConfig()
    {
        return $this->getDataStoreKeyInSchemaConfig();
    }
}
