<?php

/**
 * Functionality at the database level
 */

namespace PHPCouchDB;

/**
 * Working with a particular database is all done here.  Usually instantiated
 * by the Server object as it has the connection details we need to use.
 */


class Database
{
    protected $client;
    protected $db_name;

    const OPTION_INCLUDE_DOCS = 'include_docs';
    const OPTION_REDUCE = 'reduce';
    const OPTION_DDOC = 'ddoc';
    const OPTION_VIEW = 'view';

    /**
     * Constructor for the Database object - this is usually called by
     * Server::useDb rather than directly
     *
     * @param \GuzzleHTTP\ClientInterface $client Our HTTP client
     * @param string $db_name The datbase to connect to
     */

    public function __construct(\GuzzleHttp\ClientInterface $client, string $db_name)
    {
        $this->client = $client;
        $this->db_name = $db_name;
    }

    /**
     * Get the name of the database
     */
    public function getName() : string
    {
        return $this->db_name;
    }

    /**
     * If you need to make a request that isn't supported by this library,
     * use this method to get the client to use.  Aimed at more advanced
     * users/requirements
     */
    public function getClient() : \GuzzleHttp\ClientInterface
    {
        return $this->client;
    }

    /**
     * Format object for var_dump(), removing large properties
     */
    public function __debugInfo()
    {
        $result = get_object_vars($this);
        unset($result['client']);
        return $result;
    }

    /**
     * Fetch all the documents from the database
     *
     * @param array $options  Any modifiers needed for the query  These include:
     *      - include_docs   Defaults to true
     * @return array The array contains `PHPCouchDB\Document` objects if the
     *      include_docs parameter was set; otherwise an array of arrays with
     *      "id" and "rev" keys
     */
    public function getAllDocs($options = []) : array
    {
        $endpoint = "/" . $this->db_name . "/_all_docs";

        // grab extra params
        $query = $options;

        // convert data and set some defaults
        if (isset($query[self::OPTION_INCLUDE_DOCS])) {
            $query[self::OPTION_INCLUDE_DOCS] = $this->boolToString(
                $query[self::OPTION_INCLUDE_DOCS]
            );
        } else {
            // needs to be a string and this is our chosen default value
            $query[self::OPTION_INCLUDE_DOCS] = "true";
        }

        $response = $this->client->request("GET", $endpoint, ["query" => $query]);
        $data = $this->handleServerResponse($response);
        return $data;
    }

    /**
     * Create a new document in the database
     *
     * @param array $doc  An array representing the document's keys and values.
     *  The data can be nested arrays, lists, anything
     *  - if you include an "id" key, this will become the document ID,
     *      otherwise it'll be autogenerated
     * @return PHPCouchDB\Document a document object of your new doc
     */
    public function create($doc)
    {
        if (!is_array($doc)) {
            throw new Exception\DatabaseException('A document is required, in array format');
        }

        // do we have the ID?
        if (isset($doc['id']) && !empty($doc['id'])) {
            // remove the ID from the array, then use it in the URL with PUT
            $id = $doc['id'];
            unset($doc['id']);
            $endpoint = "/" . $this->db_name . "/" . $id;
            $verb = 'PUT';
        } else {
            // no ID, make a POST request to create the record
            $endpoint = "/" . $this->db_name . "/";
            $verb = 'POST';
        }

        try {
            $response = $this->client->request($verb, $endpoint, ['json' => $doc]);
            if ($response->getStatusCode() == 201 && $response_data = json_decode($response->getBody(), true)) {
                $newdoc = new Document($this, $doc);
                $newdoc->id = $response_data['id'];
                $newdoc->rev = $response_data['rev'];
                return $newdoc;
            }
        } catch (\GuzzleHttp\Exception\ConnectException $e) {
            throw new Exception\ServerException(
                "Could not create record.  Error: " . $e->getMessage(),
                0,
                $e
            );
        }
    }

    /**
     * Get a document whose ID you know
     *
     * @param string $id The doc's unique identifier
     * @return PHPCouchDB\Document The doc with the specified ID
     * @throws PHPCouchDB\Exception\ServerException if the response can't be understood
     * @throws PHPCouchDB\Exception\DatabaseException if the doc isn't found
     */
    public function getDocById($id) : Document
    {
        $endpoint = "/" . $this->db_name . "/" . $id;
        try {
            $response = $this->client->request("GET", $endpoint);
            if ($data = json_decode($response->getBody(), true)) {
                $doc = new Document($this, $data);
                return $doc;
            } else {
                throw new Exception\ServerException('JSON response not received or not understood');
            }
        } catch (\GuzzleHttp\Exception\ClientException $e) {
            $status = $e->getResponse()->getStatusCode();
            if ($status == 404) {
                // not really a disaster, the doc just isn't there so throw specific exception
                throw new Exception\DocumentNotFoundException('Document not found');
            } else {
                throw new Exception\DatabaseException('The document could not be retrieved', 0, $e);
            }
        }
    }

    /**
     * Get data from a view, either docs or grouped data
     *
     * @param array $options Must include `ddoc` and `view`, also supports any
     *   other query parameters that should be passed to the view (e.g. limit)
     * @return array If there are documents, an array of \PHPCouchDB\Document
     *   objects, otherwise an array as appropriate
     */
    public function getView($options = []) : array
    {
        // check we have ddoc and view name
        if (!isset($options[self::OPTION_DDOC])) {
            throw new Exception\ServerException(
                'ddoc is a required parameter for getView'
            );
        }
        if (!isset($options[self::OPTION_VIEW])) {
            throw new Exception\ServerException(
                'view is a required parameter for getView'
            );
        }

        $endpoint = "/" . $this->db_name . "/_design/" . $options[self::OPTION_DDOC]
            . "/_view/" . $options[self::OPTION_VIEW];

        // grab extra params
        $query = [];
        foreach ($options as $key => $value) {
            // skip the values we need for the URL, pass the rest through
            if (!in_array($key, [self::OPTION_DDOC, self::OPTION_VIEW])) {
                $query[$key] = $value;
            }
        }

        // convert data and set some defaults
        if (isset($query[self::OPTION_INCLUDE_DOCS])) {
            $query[self::OPTION_INCLUDE_DOCS] = $this->boolToString(
                $query[self::OPTION_INCLUDE_DOCS]
            );
        } else {
            // needs to be a string and this is our chosen default value
            $query[self::OPTION_INCLUDE_DOCS] = "true";
        }

        if (isset($query[self::OPTION_REDUCE])) {
            $query[self::OPTION_REDUCE] = $this->boolToString(
                $query[self::OPTION_REDUCE]
            );
        } else {
            // needs to be a string and this is our chosen default value
            $query[self::OPTION_REDUCE] = "true";
        }

        $response = $this->client->request("GET", $endpoint, ["query" => $query]);
        $data = $this->handleServerResponse($response);
        return $data;
    }

    /**
     * Take a result set, work out if it contains docs, doc tombstones, or
     * data that we shouldn't mess with, and return \Document objects or arrays
     * as appropriate
     *
     * @param \GuzzleHttp\Message\ResponseInterface $response What we got back
     *  from making a Guzzle request to the CouchDB server
     * @return array An array whose contents depends on the response content:
     *  - an array of full CouchDB docs: returns array of \PHPCouchDB\Document
     *  - an array of id/rev pairs from docs: returns an array of arrays containing id/rev
     *  - an array of anything else: returns it just as an array
     */
    protected function handleServerResponse($response) : array
    {
        if ($response->getStatusCode() == 200) {
            // try to decode JSON
            if ($json_data = json_decode($response->getBody(), true)) {
                if (isset($json_data['rows'][0]['doc'])) {
                    // we have some data - extract the docs to return
                    $docs = [];
                    foreach ($json_data['rows'] as $document) {
                        $docs[] = new Document($this, $document["doc"]);
                    }
                    return $docs;
                } elseif (isset($json_data['rows'][0]['value']['rev'])) {
                    // assume these are doc signposts
                    $docs = [];
                    foreach ($json_data['rows'] as $item) {
                        $doc = [];
                        $doc['id'] = $item['id'];
                        $doc['rev'] = $item['value']['rev'];
                        $docs[] = $doc;
                    }
                    return $docs;
                } else {
                    // no docs, just return some basic info
                    return $json_data["rows"];
                }
            } else {
                throw new Exception\ServerException('JSON response not received or not understood');
            }
        }
    }

    /**
     * Convert truthy things to "true" and the rest to "false" because
     * Guzzle doesn't send booleans as words
     *
     * @param mixed $value The value to use
     * @return string A string either "true" or "false"
     */
    protected function boolToString($value) : string
    {
        if ($value) {
            return "true";
        } else {
            return "false";
        }
    }
}
