<?php

namespace SilverStripe\RestfulServer\DataFormatter;

use SilverStripe\RestfulServer\RestfulServer;
use SilverStripe\View\ArrayData;
use SilverStripe\Core\Convert;
use SilverStripe\RestfulServer\DataFormatter;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\Control\Director;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\FieldType;

/**
 * Formats a DataObject's member fields into a JSON string
 */
class JSONDataFormatter extends DataFormatter
{
    /**
     * @config
     * @todo pass this from the API to the data formatter somehow
     */
    private static $api_base = "api/v1/";

    protected $outputContentType = 'application/json';

    /**
     * @return array
     */
    public function supportedExtensions()
    {
        return array(
            'json',
            'js'
        );
    }

    /**
     * @return array
     */
    public function supportedMimeTypes()
    {
        return array(
            'application/json',
            'text/x-json'
        );
    }

    /**
     * @param $array
     * @return string
     */
    public function convertArray($array)
    {
        return json_encode($array);
    }

    /**
     * Generate a JSON representation of the given {@link DataObject}.
     *
     * @param DataObject $obj   The object
     * @param Array $fields     If supplied, only fields in the list will be returned
     * @param $relations        Not used
     * @return String JSON
     */
    public function convertDataObject(DataObjectInterface $obj, $fields = null, $relations = null)
    {
        return json_encode($this->convertDataObjectToJSONObject($obj, $fields, $relations));
    }

    /**
     * Internal function to do the conversion of a single data object. It builds an empty object and dynamically
     * adds the properties it needs to it. If it's done as a nested array, json_encode or equivalent won't use
     * JSON object notation { ... }.
     * @param DataObjectInterface $obj
     * @param  $fields
     * @param  $relations
     * @return EmptyJSONObject
     */
    public function convertDataObjectToJSONObject(DataObjectInterface $obj, $fields = null, $relations = null)
    {
        $className = get_class($obj);
        $id = $obj->ID;

        $serobj = ArrayData::array_to_object();

        foreach ($this->getFieldsForObj($obj) as $fieldName => $fieldType) {
            // Field filtering
            if ($fields && !in_array($fieldName, $fields ?? [])) {
                continue;
            }

            $fieldValue = self::cast($obj->obj($fieldName));
            $mappedFieldName = $this->getFieldAlias($className, $fieldName);
            $serobj->$mappedFieldName = $fieldValue;
        }

        if ($this->relationDepth > 0) {
            foreach ($obj->hasOne() as $relName => $relClass) {
                if (!$relClass::config()->get('api_access')) {
                    continue;
                }

                // Field filtering
                if ($fields && !in_array($relName, $fields ?? [])) {
                    continue;
                }
                if ($this->customRelations && !in_array($relName, $this->customRelations ?? [])) {
                    continue;
                }
                if ($obj->$relName() && (!$obj->$relName()->exists() || !$obj->$relName()->canView())) {
                    continue;
                }

                $fieldName = $relName . 'ID';
                $rel = $this->config()->api_base;
                $rel .= $obj->$fieldName
                    ? $this->sanitiseClassName($relClass) . '/' . $obj->$fieldName
                    : $this->sanitiseClassName($className) . "/$id/$relName";
                $href = Director::absoluteURL($rel);
                $serobj->$relName = ArrayData::array_to_object(array(
                    "className" => $relClass,
                    "href" => "$href.json",
                    "id" => self::cast($obj->obj($fieldName))
                ));
            }

            foreach ($obj->hasMany() + $obj->manyMany() as $relName => $relClass) {
                $relClass = RestfulServer::parseRelationClass($relClass);

                //remove dot notation from relation names
                $parts = explode('.', $relClass ?? '');
                $relClass = array_shift($parts);

                if (!$relClass::config()->get('api_access')) {
                    continue;
                }

                // Field filtering
                if ($fields && !in_array($relName, $fields ?? [])) {
                    continue;
                }
                if ($this->customRelations && !in_array($relName, $this->customRelations ?? [])) {
                    continue;
                }

                $innerParts = array();
                $items = $obj->$relName();
                foreach ($items as $item) {
                    if (!$item->canView()) {
                        continue;
                    }
                    $rel = $this->config()->api_base . $this->sanitiseClassName($relClass) . "/$item->ID";
                    $href = Director::absoluteURL($rel);
                    $innerParts[] = ArrayData::array_to_object(array(
                        "className" => $relClass,
                        "href" => "$href.json",
                        "id" => $item->ID
                    ));
                }
                $serobj->$relName = $innerParts;
            }
        }

        return $serobj;
    }

    /**
     * Generate a JSON representation of the given {@link SS_List}.
     *
     * @param SS_List $set
     * @return String XML
     */
    public function convertDataObjectSet(SS_List $set, $fields = null)
    {
        $items = array();
        foreach ($set as $do) {
            if (!$do->canView()) {
                continue;
            }
            $items[] = $this->convertDataObjectToJSONObject($do, $fields);
        }

        $serobj = ArrayData::array_to_object(array(
            "totalSize" => (is_numeric($this->totalSize)) ? $this->totalSize : null,
            "items" => $items
        ));

        return json_encode($serobj);
    }

    /**
     * @param string $strData
     * @return array|bool|void
     */
    public function convertStringToArray($strData)
    {
        return json_decode($strData ?? '', true);
    }

    public static function cast(FieldType\DBField $dbfield)
    {
        switch (true) {
            case $dbfield instanceof FieldType\DBInt:
                return (int)$dbfield->RAW();
            case $dbfield instanceof FieldType\DBFloat:
                return (float)$dbfield->RAW();
            case $dbfield instanceof FieldType\DBBoolean:
                return (bool)$dbfield->RAW();
            case is_null($dbfield->RAW()):
                return null;
        }
        return $dbfield->RAW();
    }
}