<?php
/*
* 2007-2015 PrestaShop
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
*  @author PrestaShop SA <contact@prestashop.com>
*  @copyright  2007-2015 PrestaShop SA
*  @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
*  International Registered Trademark & Property of PrestaShop SA
*/

/**
 * @todo : Create typed exception for more finer errors check
 */
class WebserviceOutputBuilderCore
{
    /**
     * @var int constant
     */
    const VIEW_LIST = 1;
    const VIEW_DETAILS = 2;

    protected $wsUrl;
    protected $output;

    /** @var WebserviceOutputInterface|WebserviceOutputXML|WebserviceOutputJSON */
    public $objectRender;
    protected $wsResource;
    protected $depth = 0;
    protected $schemaToDisplay;
    protected $fieldsToDisplay;
    protected $specificFields = array();
    protected $virtualFields = array();
    protected $statusInt;
    protected $wsParamOverrides;

    protected static $_cache_ws_parameters = array();

    /* Header properties */
    protected $headerParams = array(
        'Access-Time'    => 0,
        'X-Powered-By'    => 0,
        'PSWS-Version'    => 0,
        'Content-Type'    => 0,
    );

    /**
     * @var string Status header sent at return
     */
    protected $status;

    public function __construct($ws_url)
    {
        $this->statusInt = 200;
        $this->status = $_SERVER['SERVER_PROTOCOL'].' 200 OK';
        $this->wsUrl = $ws_url;
        $this->wsParamOverrides = array();
    }

    /**
     * Set the render object for set the output format.
     * Set the Content-type for the http header.
     *
     * @param WebserviceOutputInterface $obj_render
     * @throw WebserviceException if the object render is not an instance of WebserviceOutputInterface
     *
     * @return WebserviceOutputBuilder
     * @throws WebserviceException
     */
    public function setObjectRender(WebserviceOutputInterface $obj_render)
    {
        if (!$obj_render instanceof WebserviceOutputInterface) {
            throw new WebserviceException('Obj_render param must be an WebserviceOutputInterface object type', array(83, 500));
        }

        $this->objectRender = $obj_render;
        $this->objectRender->setWsUrl($this->wsUrl);
        if ($this->objectRender->getContentType()) {
            $this->setHeaderParams('Content-Type', $this->objectRender->getContentType());
        }
        return $this;
    }

    /**
     * getter
     * @return WebserviceOutputInterface
     */
    public function getObjectRender()
    {
        return $this->objectRender;
    }

    /**
     * Need to have the resource list to get the class name for an entity,
     * To build
     *
     * @param array $resources
     *
     * @return WebserviceOutputBuilder
     */
    public function setWsResources($resources)
    {
        $this->wsResource = $resources;
        return $this;
    }

    /**
     * This method return an array with each http header params for a content.
     * This check each required params.
     *
     * If this method is overrided don't forget to check required specific params (for xml etc...)
     *
     * @return array
     */
    public function buildHeader()
    {
        $return = array();
        $return[] = $this->status;
        foreach ($this->headerParams as $key => $param) {
            $return[] = trim($key).': '.$param;
        }
        return $return;
    }

    /**
     * @param string $key The normalized key expected for an http response
     * @param string $value
     *
     * @return WebserviceOutputBuilder
     * @throws WebserviceException If the key or the value are corrupted (use Validate::isCleanHtml method)
     */
    public function setHeaderParams($key, $value)
    {
        if (!Validate::isCleanHtml($key) || !Validate::isCleanHtml($value)) {
            throw new WebserviceException('the key or your value is corrupted.', array(94, 500));
        }
        $this->headerParams[$key] = $value;
        return $this;
    }

    /**
     * @param null|string $key if null get all header params otherwise the params specified by the key
     * @throw WebserviceException if the key is corrupted (use Validate::isCleanHtml method)
     * @throw WebserviceException if the asked key does'nt exists.
     * @return array|string
     */
    public function getHeaderParams($key = null)
    {
        $return = '';

        if (!is_null($key)) {
            if (!Validate::isCleanHtml($key)) {
                throw new WebserviceException('the key you write is a corrupted text.', array(95, 500));
            }
            if (!array_key_exists($key, $this->headerParams)) {
                throw new WebserviceException(sprintf('The key %s does\'nt exist', $key), array(96, 500));
            }
            $return = $this->headerParams[$key];
        } else {
            $return = $this->headerParams;
        }

        return $return;
    }

    /**
     * Delete all Header parameters previously set.
     *
     * @return WebserviceOutputBuilder
     */
    public function resetHeaderParams()
    {
        $this->headerParams = array();
        return $this;
    }

    /**
     * @return string the normalized status for http request
     */
    public function getStatus()
    {
        return $this->status;
    }

    public function getStatusInt()
    {
        return $this->statusInt;
    }
    /**
     * Set the return header status
     *
     * @param int $num the Http status code
     * @return void
     */
    public function setStatus($num)
    {
        $this->statusInt = (int)$num;
        switch ($num) {
            case 200 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 200 OK';
                break;
            case 201 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 201 Created';
                break;
            case 204 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 204 No Content';
                break;
            case 304 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 304 Not Modified';
                break;
            case 400 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 400 Bad Request';
                break;
            case 401 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 401 Unauthorized';
                break;
            case 403 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 403 Forbidden';
                break;
            case 404 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 404 Not Found';
                break;
            case 405 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 405 Method Not Allowed';
                break;
            case 500 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 500 Internal Server Error';
                break;
            case 501 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 501 Not Implemented';
                break;
            case 503 :
                $this->status = $_SERVER['SERVER_PROTOCOL'].' 503 Service Unavailable';
                break;
        }
    }

    /**
     * Build errors output using an error array
     *
     * @param array $errors
     * @return string output in the format specified by WebserviceOutputBuilder::objectRender
     */
    public function getErrors($errors)
    {
        if (!empty($errors)) {
            if (isset($this->objectRender)) {
                $str_output = $this->objectRender->renderErrorsHeader();
                foreach ($errors as $error) {
                    if (is_array($error)) {
                        $str_output .= $this->objectRender->renderErrors($error[1], $error[0]);
                    } else {
                        $str_output .= $this->objectRender->renderErrors($error);
                    }
                }
                $str_output .= $this->objectRender->renderErrorsFooter();
                $str_output = $this->objectRender->overrideContent($str_output);
            } else {
                $str_output = '<pre>'.print_r($errors, true).'</pre>';
            }
        }
        return $str_output;
    }

    /**
     * Build the resource list in the output format specified by WebserviceOutputBuilder::objectRender
     * @param $key_permissions
     * @return string
     */
    public function getResourcesList($key_permissions)
    {
        if (is_null($this->wsResource)) {
            throw new WebserviceException('You must set web service resource for get the resources list.', array(82, 500));
        }
        $output = '';
        $more_attr = array('shopName' => htmlspecialchars(Configuration::get('PS_SHOP_NAME')));
        $output .= $this->objectRender->renderNodeHeader('api', array(), $more_attr);
        foreach ($this->wsResource as $resourceName => $resource) {
            if (in_array($resourceName, array_keys($key_permissions))) {
                $more_attr = array(
                    'xlink_resource'    => $this->wsUrl.$resourceName,
                    'get'                => (in_array('GET', $key_permissions[$resourceName]) ? 'true' : 'false'),
                    'put'                => (in_array('PUT', $key_permissions[$resourceName]) ? 'true' : 'false'),
                    'post'                => (in_array('POST', $key_permissions[$resourceName]) ? 'true' : 'false'),
                    'delete'            => (in_array('DELETE', $key_permissions[$resourceName]) ? 'true' : 'false'),
                    'head'                => (in_array('HEAD', $key_permissions[$resourceName]) ? 'true' : 'false'),
                );
                $output .= $this->objectRender->renderNodeHeader($resourceName, array(), $more_attr);

                $output .= $this->objectRender->renderNodeHeader('description', array(), $more_attr);
                $output .= $resource['description'];
                $output .= $this->objectRender->renderNodeFooter('description', array());

                if (!isset($resource['specific_management']) || !$resource['specific_management']) {
                    $more_attr_schema = array(
                        'xlink_resource'    => $this->wsUrl.$resourceName.'?schema=blank',
                        'type'                => 'blank',
                    );
                    $output .= $this->objectRender->renderNodeHeader('schema', array(), $more_attr_schema, false);
                    $more_attr_schema = array(
                        'xlink_resource'    => $this->wsUrl.$resourceName.'?schema=synopsis',
                        'type'                => 'synopsis',
                    );
                    $output .= $this->objectRender->renderNodeHeader('schema', array(), $more_attr_schema, false);
                }
                $output .= $this->objectRender->renderNodeFooter($resourceName, array());
            }
        }
        $output .= $this->objectRender->renderNodeFooter('api', array());
        $output = $this->objectRender->overrideContent($output);
        return $output;
    }

    public function registerOverrideWSParameters($wsrObject, $method)
    {
        $this->wsParamOverrides[] = array('object' => $wsrObject, 'method' => $method);
    }

    /**
     * Method is used for each content type
     * Different content types are :
     * 		- list of entities,
     * 		- tree diagram of entity details (full or minimum),
     * 		- schema (synopsis & blank),
     *
     * @param array $objects each object created by entity asked
     * 		  @see WebserviceOutputBuilder::executeEntityGetAndHead
     * @param null|string $schema_to_display if null display the entities list or entity details.
     * @param string|array $fields_to_display the fields allow for the output
     * @param int $depth depth for the tree diagram output.
     * @param int $type_of_view use the 2 constants WebserviceOutputBuilder::VIEW_LIST WebserviceOutputBuilder::VIEW_DETAILS
     * @return string in the output format specified by WebserviceOutputBuilder::objectRender
     */
    public function getContent($objects, $schema_to_display = null, $fields_to_display = 'minimum', $depth = 0, $type_of_view = self::VIEW_LIST, $override = true)
    {
        $this->fieldsToDisplay = $fields_to_display;
        $this->depth = $depth;
        $output = '';

        if ($schema_to_display != null) {
            $this->schemaToDisplay = $schema_to_display;
            $this->objectRender->setSchemaToDisplay($this->schemaToDisplay);

            // If a shema is asked the view must be an details type
            $type_of_view = self::VIEW_DETAILS;
        }

        $class = get_class($objects['empty']);
        if (!isset(WebserviceOutputBuilder::$_cache_ws_parameters[$class])) {
            WebserviceOutputBuilder::$_cache_ws_parameters[$class] = $objects['empty']->getWebserviceParameters();
        }
        $ws_params = WebserviceOutputBuilder::$_cache_ws_parameters[$class];

        foreach ($this->wsParamOverrides as $p) {
            $object = $p['object'];
            $ws_params = $object->{$p['method']}($ws_params);
        }

        // If a list is asked, need to wrap with a plural node
        if ($type_of_view === self::VIEW_LIST) {
            $output .= $this->setIndent($depth).$this->objectRender->renderNodeHeader($ws_params['objectsNodeName'], $ws_params);
        }

        if (is_null($this->schemaToDisplay)) {
            foreach ($objects as $key => $object) {
                if ($key !== 'empty') {
                    if ($this->fieldsToDisplay === 'minimum') {
                        $output .= $this->renderEntityMinimum($object, $depth);
                    } else {
                        $output .= $this->renderEntity($object, $depth);
                    }
                }
            }
        } else {
            $output .= $this->renderSchema($objects['empty'], $ws_params);
        }

        // If a list is asked, need to wrap with a plural node
        if ($type_of_view === self::VIEW_LIST) {
            $output .= $this->setIndent($depth).$this->objectRender->renderNodeFooter($ws_params['objectsNodeName'], $ws_params);
        }

        if ($override) {
            $output = $this->objectRender->overrideContent($output);
        }
        return $output;
    }

    /**
     * Create the tree diagram with no details
     *
     * @param ObjectModel $object create by the entity
     * @param int $depth the depth for the tree diagram
     * @return string
     */
    public function renderEntityMinimum($object, $depth)
    {
        $class = get_class($object);
        if (!isset(WebserviceOutputBuilder::$_cache_ws_parameters[$class])) {
            WebserviceOutputBuilder::$_cache_ws_parameters[$class] = $object->getWebserviceParameters();
        }
        $ws_params = WebserviceOutputBuilder::$_cache_ws_parameters[$class];

        $more_attr['id'] = $object->id;
        $more_attr['xlink_resource'] = $this->wsUrl.$ws_params['objectsNodeName'].'/'.$object->id;
        $output = $this->setIndent($depth).$this->objectRender->renderNodeHeader($ws_params['objectNodeName'], $ws_params, $more_attr, false);
        return $output;
    }

    /**
     * Build a schema blank or synopsis
     *
     * @param ObjectModel $object create by the entity
     * @param array $ws_params webserviceParams from the entity
     * @return string
     */
    protected function renderSchema($object, $ws_params)
    {
        $output = $this->objectRender->renderNodeHeader($ws_params['objectNodeName'], $ws_params);
        foreach ($ws_params['fields'] as $field_name => $field) {
            $output .= $this->renderField($object, $ws_params, $field_name, $field, 0);
        }
        if (isset($ws_params['associations']) && count($ws_params['associations']) > 0) {
            $this->fieldsToDisplay = 'full';
            $output .= $this->renderAssociations($object, 0, $ws_params['associations'], $ws_params);
        }
        $output .= $this->objectRender->renderNodeFooter($ws_params['objectNodeName'], $ws_params);
        return $output;
    }

    /**
     * Build the entity detail.
     *
     * @param ObjectModel $object create by the entity
     * @param int $depth the depth for the tree diagram
     * @return string
     */
    public function renderEntity($object, $depth)
    {
        $output = '';

        $class = get_class($object);
        if (!isset(WebserviceOutputBuilder::$_cache_ws_parameters[$class])) {
            WebserviceOutputBuilder::$_cache_ws_parameters[$class] = $object->getWebserviceParameters();
        }
        $ws_params = WebserviceOutputBuilder::$_cache_ws_parameters[$class];

        foreach ($this->wsParamOverrides as $p) {
            $o = $p['object'];
            $ws_params = $o->{$p['method']}($ws_params);
        }
        $output .= $this->setIndent($depth).$this->objectRender->renderNodeHeader($ws_params['objectNodeName'], $ws_params);

        if ($object->id != 0) {
            // This to add virtual Fields for a particular entity.
            $virtual_fields = $this->addVirtualFields($ws_params['objectsNodeName'], $object);
            if (!empty($virtual_fields)) {
                $ws_params['fields'] = array_merge($ws_params['fields'], $virtual_fields);
            }

            foreach ($ws_params['fields'] as $field_name => $field) {
                if ($this->fieldsToDisplay === 'full' || array_key_exists($field_name, $this->fieldsToDisplay)) {
                    $field['object_id'] = $object->id;
                    $field['entity_name'] = $ws_params['objectNodeName'];
                    $field['entities_name'] = $ws_params['objectsNodeName'];
                    $output .= $this->renderField($object, $ws_params, $field_name, $field, $depth);
                }
            }
        }
        $subexists = false;
        if (is_array($this->fieldsToDisplay)) {
            foreach ($this->fieldsToDisplay as $fields) {
                if (is_array($fields)) {
                    $subexists = true;
                }
            }
        }

        if (isset($ws_params['associations'])
            && ($this->fieldsToDisplay == 'full'
            || $subexists)) {
            $output .= $this->renderAssociations($object, $depth, $ws_params['associations'], $ws_params);
        }

        $output .= $this->setIndent($depth).$this->objectRender->renderNodeFooter($ws_params['objectNodeName'], $ws_params);
        return $output;
    }

    /**
     * Build a field and use recursivity depend on the depth parameter.
     *
     * @param ObjectModel $object create by the entity
     * @param array $ws_params webserviceParams from the entity
     * @param string $field_name
     * @param array $field
     * @param int $depth
     * @return string
     */
    protected function renderField($object, $ws_params, $field_name, $field, $depth)
    {
        $output = '';
        $show_field = true;

        if (isset($ws_params['hidden_fields']) && in_array($field_name, $ws_params['hidden_fields'])) {
            return;
        }

        if ($this->schemaToDisplay === 'synopsis') {
            $field['synopsis_details'] = $this->getSynopsisDetails($field);
            if ($field_name === 'id') {
                $show_field = false;
            }
        }
        if ($this->schemaToDisplay === 'blank') {
            if (isset($field['setter']) && !$field['setter']) {
                $show_field = false;
            }
        }

        // don't set any value for a schema
        if (isset($field['synopsis_details']) || $this->schemaToDisplay === 'blank') {
            $field['value'] = '';
            if (isset($field['xlink_resource'])) {
                unset($field['xlink_resource']);
            }
        } elseif (isset($field['getter']) && $object != null && method_exists($object, $field['getter'])) {
            $field['value'] = $object->$field['getter']();
        } elseif (!isset($field['value'])) {
            $field['value'] = $object->$field_name;
        }

        // this apply specific function for a particular field on a choosen entity
        $field = $this->overrideSpecificField($ws_params['objectsNodeName'], $field_name, $field, $object, $ws_params);

        // don't display informations for a not existant id
        if (substr($field['sqlId'], 0, 3) == 'id_' && !$field['value']) {
            if ($field['value'] === null) {
                $field['value'] = '';
            }
            // delete the xlink except for schemas
            if (isset($field['xlink_resource']) && is_null($this->schemaToDisplay)) {
                unset($field['xlink_resource']);
            }
        }
        // set "id" for each node name which display the id of the entity
        if ($field_name === 'id') {
            $field['sqlId'] = 'id';
        }

        // don't display the node id for a synopsis schema
        if ($show_field) {
            $output .= $this->setIndent($depth - 1).$this->objectRender->renderField($field);
        }
        return $output;
    }

    /**
     *
     *
     * @param $object
     * @param $depth
     * @param $associations
     * @param $ws_params
     * @return string
     */
    protected function renderAssociations($object, $depth, $associations, $ws_params)
    {
        $output = $this->objectRender->renderAssociationWrapperHeader();
        foreach ($associations as $assoc_name => $association) {
            if ($this->fieldsToDisplay == 'full' || is_array($this->fieldsToDisplay) && array_key_exists($assoc_name, $this->fieldsToDisplay)) {
                $getter = $association['getter'];
                $objects_assoc = array();

                $fields_assoc = array();
                if (isset($association['fields'])) {
                    $fields_assoc = $association['fields'];
                }

                $parent_details = array(
                        'object_id'    => $object->id,
                        'entity_name'    => $ws_params['objectNodeName'],
                        'entities_name'    => $ws_params['objectsNodeName'],
                    );

                if (is_array($getter)) {
                    $association_resources = call_user_func($getter, $object);
                    if (is_array($association_resources) && !empty($association_resources)) {
                        foreach ($association_resources as $association_resource) {
                            $objects_assoc[] = $association_resource;
                        }
                    }
                } else {
                    if (method_exists($object, $getter) && is_null($this->schemaToDisplay)) {
                        $association_resources = $object->$getter();
                        if (is_array($association_resources) && !empty($association_resources)) {
                            foreach ($association_resources as $association_resource) {
                                $objects_assoc[] = $association_resource;
                            }
                        }
                    } else {
                        $objects_assoc[] = '';
                    }
                }

                $class_name = null;
                if (isset($this->wsResource[$assoc_name]['class']) && class_exists($this->wsResource[$assoc_name]['class'], true)) {
                    $class_name = $this->wsResource[$assoc_name]['class'];
                }
                $output_details = '';
                foreach ($objects_assoc as $object_assoc) {
                    if ($depth == 0 || $class_name === null) {
                        $value = null;
                        if (!empty($object_assoc)) {
                            $value = $object_assoc;
                        }
                        if (empty($fields_assoc)) {
                            $fields_assoc = array(array('id' => $value['id']));
                        }
                        $output_details .= $this->renderFlatAssociation($object, $depth, $assoc_name, $association['resource'], $fields_assoc, $value, $parent_details);
                    } else {
                        foreach ($object_assoc as $id) {
                            if ($class_name !== null) {
                                $child_object = new $class_name($id);
                                $output_details .= $this->renderEntity($child_object, ($depth - 2 ? 0 : $depth - 2));
                            }
                        }
                    }
                }
                if ($output_details != '') {
                    $output .= $this->setIndent($depth).$this->objectRender->renderAssociationHeader($object, $ws_params, $assoc_name);
                    $output .= $output_details;
                    $output .= $this->setIndent($depth).$this->objectRender->renderAssociationFooter($object, $ws_params, $assoc_name);
                } else {
                    $output .= $this->setIndent($depth).$this->objectRender->renderAssociationHeader($object, $ws_params, $assoc_name, true);
                }
            }
        }
        $output .= $this->objectRender->renderAssociationWrapperFooter();
        return $output;
    }

    protected function renderFlatAssociation($object, $depth, $assoc_name, $resource_name, $fields_assoc, $object_assoc, $parent_details)
    {
        $output = '';
        $more_attr = array();
        if (isset($this->wsResource[$assoc_name]) && is_null($this->schemaToDisplay)) {
            if ($assoc_name == 'images') {
                if ($parent_details['entities_name'] == 'combinations') {
                    $more_attr['xlink_resource'] = $this->wsUrl.$assoc_name.'/products/'.$object->id_product.'/'.$object_assoc['id'];
                } else {
                    $more_attr['xlink_resource'] = $this->wsUrl.$assoc_name.'/'.$parent_details['entities_name'].'/'.$parent_details['object_id'].'/'.$object_assoc['id'];
                }
            } else {
                $more_attr['xlink_resource'] = $this->wsUrl.$assoc_name.'/'.$object_assoc['id'];
            }
        }
        $output .= $this->setIndent($depth - 1).$this->objectRender->renderNodeHeader($resource_name, array(), $more_attr);

        foreach ($fields_assoc as $field_name => $field) {
            if (!is_array($this->fieldsToDisplay) || in_array($field_name, $this->fieldsToDisplay[$assoc_name])) {
                if ($field_name == 'id' && !isset($field['sqlId'])) {
                    $field['sqlId'] = 'id';
                    $field['value'] = $object_assoc['id'];
                } elseif (!isset($field['sqlId'])) {
                    $field['sqlId'] = $field_name;
                    $field['value'] = $object_assoc[$field_name];
                }
                $field['entities_name'] = $assoc_name;
                $field['entity_name'] = $resource_name;

                if (!is_null($this->schemaToDisplay)) {
                    $field['synopsis_details'] = $this->getSynopsisDetails($field);
                }
                $field['is_association'] = true;
                $output .= $this->setIndent($depth - 1).$this->objectRender->renderField($field);
            }
        }
        $output .= $this->setIndent($depth - 1).$this->objectRender->renderNodeFooter($resource_name, array());
        return $output;
    }

    public function setIndent($depth)
    {
        $string = '';
        $number_of_tabs = $this->depth - $depth;
        for ($i = 0; $i < $number_of_tabs; $i++) {
            $string .= "\t";
        }
        return $string;
    }

    public function getSynopsisDetails($field)
    {
        $arr_details = '';
        if (array_key_exists('required', $field) && $field['required']) {
            $arr_details['required'] = 'true';
        }
        if (array_key_exists('maxSize', $field) && $field['maxSize']) {
            $arr_details['maxSize'] = $field['maxSize'];
        }
        if (array_key_exists('validateMethod', $field) && $field['validateMethod']) {
            $arr_details['format'] = $field['validateMethod'];
        }
        if (array_key_exists('setter', $field) && !$field['setter']) {
            $arr_details['readOnly'] = 'true';
        }
        return $arr_details;
    }

    /**
     * @param string|object $object
     * @param string $method
     * @param $field_name
     * @param $entity_name
     *
     * @return WebserviceOutputBuilder
     * @throws Exception
     * @throws WebserviceException
     */
    public function setSpecificField($object, $method, $field_name, $entity_name)
    {
        try {
            $this->validateObjectAndMethod($object, $method);
        } catch (WebserviceException $e) {
            throw $e;
        }

        $this->specificFields[$field_name] = array('entity'=>$entity_name, 'object' => $object, 'method' => $method, 'type' => gettype($object));
        return $this;
    }
    protected function validateObjectAndMethod($object, $method)
    {
        if (is_string($object) && !class_exists($object)) {
            throw new WebserviceException('The object you want to set in '.__METHOD__.' is not allowed.', array(98, 500));
        }
        if (!method_exists($object, $method)) {
            throw new WebserviceException('The method you want to set in '.__METHOD__.' is not allowed.', array(99, 500));
        }
    }
    public function getSpecificField()
    {
        return $this->specificFields;
    }
    protected function overrideSpecificField($entity_name, $field_name, $field, $entity_object, $ws_params)
    {
        if (array_key_exists($field_name, $this->specificFields) && $this->specificFields[$field_name]['entity'] == $entity_name) {
            if ($this->specificFields[$field_name]['type'] == 'string') {
                $object = new $this->specificFields[$field_name]['object']();
            } elseif ($this->specificFields[$field_name]['type'] == 'object') {
                $object = $this->specificFields[$field_name]['object'];
            }

            $field = $object->{$this->specificFields[$field_name]['method']}($field, $entity_object, $ws_params);
        }
        return $field;
    }
    public function setVirtualField($object, $method, $entity_name, $parameters)
    {
        try {
            $this->validateObjectAndMethod($object, $method);
        } catch (WebserviceException $e) {
            throw $e;
        }

        $this->virtualFields[$entity_name][] = array('parameters' => $parameters, 'object' => $object, 'method' => $method, 'type' => gettype($object));
    }

    public function getVirtualFields()
    {
        return $this->virtualFields;
    }

    public function addVirtualFields($entity_name, $entity_object)
    {
        $arr_return = array();
        $virtual_fields = $this->getVirtualFields();
        if (array_key_exists($entity_name, $virtual_fields)) {
            foreach ($virtual_fields[$entity_name] as $function_infos) {
                if ($function_infos['type'] == 'string') {
                    $object = new $function_infos['object']();
                } elseif ($function_infos['type'] == 'object') {
                    $object = $function_infos['object'];
                }

                $return_fields = $object->{$function_infos['method']}($entity_object, $function_infos['parameters']);
                foreach ($return_fields as $field_name => $value) {
                    if (Validate::isConfigName($field_name)) {
                        $arr_return[$field_name] = $value;
                    } else {
                        throw new WebserviceException('Name for the virtual field is not allow', array(128, 400));
                    }
                }
            }
        }
        return $arr_return;
    }

    public function setFieldsToDisplay($fields)
    {
        $this->fieldsToDisplay = $fields;
    }
}
