<?php

namespace LdapRecord;

use LDAP\Connection as RawLdapConnection;

/** @psalm-suppress UndefinedClass */
class Ldap implements LdapInterface
{
    use HandlesConnection, DetectsErrors;

    /**
     * @inheritdoc
     */
    public function getEntries($searchResults)
    {
        return $this->executeFailableOperation(function () use ($searchResults) {
            return ldap_get_entries($this->connection, $searchResults);
        });
    }

    /**
     * Retrieves the first entry from a search result.
     *
     * @see http://php.net/manual/en/function.ldap-first-entry.php
     *
     * @param resource $searchResults
     *
     * @return resource
     */
    public function getFirstEntry($searchResults)
    {
        return $this->executeFailableOperation(function () use ($searchResults) {
            return ldap_first_entry($this->connection, $searchResults);
        });
    }

    /**
     * Retrieves the next entry from a search result.
     *
     * @see http://php.net/manual/en/function.ldap-next-entry.php
     *
     * @param resource $entry
     *
     * @return resource
     */
    public function getNextEntry($entry)
    {
        return $this->executeFailableOperation(function () use ($entry) {
            return ldap_next_entry($this->connection, $entry);
        });
    }

    /**
     * Retrieves the ldap entry's attributes.
     *
     * @see http://php.net/manual/en/function.ldap-get-attributes.php
     *
     * @param resource $entry
     *
     * @return array|false
     */
    public function getAttributes($entry)
    {
        return $this->executeFailableOperation(function () use ($entry) {
            return ldap_get_attributes($this->connection, $entry);
        });
    }

    /**
     * Returns the number of entries from a search result.
     *
     * @see http://php.net/manual/en/function.ldap-count-entries.php
     *
     * @param resource $searchResults
     *
     * @return int
     */
    public function countEntries($searchResults)
    {
        return $this->executeFailableOperation(function () use ($searchResults) {
            return ldap_count_entries($this->connection, $searchResults);
        });
    }

    /**
     * Compare value of attribute found in entry specified with DN.
     *
     * @see http://php.net/manual/en/function.ldap-compare.php
     *
     * @param string $dn
     * @param string $attribute
     * @param string $value
     *
     * @return mixed
     */
    public function compare($dn, $attribute, $value)
    {
        return $this->executeFailableOperation(function () use ($dn, $attribute, $value) {
            return ldap_compare($this->connection, $dn, $attribute, $value);
        });
    }

    /**
     * @inheritdoc
     */
    public function getLastError()
    {
        if (! $this->connection) {
            return null;
        }

        return ldap_error($this->connection);
    }

    /**
     * @inheritdoc
     */
    public function getDetailedError()
    {
        if (! $number = $this->errNo()) {
            return null;
        }

        $this->getOption(LDAP_OPT_DIAGNOSTIC_MESSAGE, $message);

        return new DetailedError($number, $this->err2Str($number), $message);
    }

    /**
     * Get all binary values from the specified result entry.
     *
     * @see http://php.net/manual/en/function.ldap-get-values-len.php
     *
     * @param $entry
     * @param $attribute
     *
     * @return array
     */
    public function getValuesLen($entry, $attribute)
    {
        return $this->executeFailableOperation(function () use ($entry, $attribute) {
            return ldap_get_values_len($this->connection, $entry, $attribute);
        });
    }

    /**
     * @inheritdoc
     */
    public function setOption($option, $value)
    {
        return ldap_set_option($this->connection, $option, $value);
    }

    /**
     * @inheritdoc
     */
    public function getOption($option, &$value = null)
    {
        ldap_get_option($this->connection, $option, $value);

        return $value;
    }

    /**
     * Set a callback function to do re-binds on referral chasing.
     *
     * @see http://php.net/manual/en/function.ldap-set-rebind-proc.php
     *
     * @param callable $callback
     *
     * @return bool
     */
    public function setRebindCallback(callable $callback)
    {
        return ldap_set_rebind_proc($this->connection, $callback);
    }

    /**
     * @inheritdoc
     */
    public function startTLS()
    {
        return $this->executeFailableOperation(function () {
            return ldap_start_tls($this->connection);
        });
    }

    /**
     * @inheritdoc
     */
    public function connect($hosts = [], $port = 389)
    {
        $this->bound = false;

        $this->host = $this->makeConnectionUris($hosts, $port);

        return $this->connection = $this->executeFailableOperation(function () {
            return ldap_connect($this->host);
        });
    }

    /**
     * @inheritdoc
     */
    public function close()
    {
        $result = (is_resource($this->connection) || $this->connection instanceof RawLdapConnection)
            ? @ldap_close($this->connection)
            : false;

        $this->connection = null;
        $this->bound = false;
        $this->host = null;

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = LDAP_DEREF_NEVER, $serverControls = [])
    {
        return $this->executeFailableOperation(function () use (
            $dn,
            $filter,
            $fields,
            $onlyAttributes,
            $size,
            $time,
            $deref,
            $serverControls
        ) {
            return empty($serverControls)
                ? ldap_search($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref)
                : ldap_search($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref, $serverControls);
        });
    }

    /**
     * @inheritdoc
     */
    public function listing($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = LDAP_DEREF_NEVER, $serverControls = [])
    {
        return $this->executeFailableOperation(function () use (
            $dn,
            $filter,
            $fields,
            $onlyAttributes,
            $size,
            $time,
            $deref,
            $serverControls
        ) {
            return empty($serverControls)
                ? ldap_list($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref)
                : ldap_list($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref, $serverControls);
        });
    }

    /**
     * @inheritdoc
     */
    public function read($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = LDAP_DEREF_NEVER, $serverControls = [])
    {
        return $this->executeFailableOperation(function () use (
            $dn,
            $filter,
            $fields,
            $onlyAttributes,
            $size,
            $time,
            $deref,
            $serverControls
        ) {
            return empty($serverControls)
                ? ldap_read($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref)
                : ldap_read($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref, $serverControls);
        });
    }

    /**
     * @inheritdoc
     */
    public function parseResult($result, &$errorCode, &$dn, &$errorMessage, &$referrals, &$serverControls = [])
    {
        return $this->executeFailableOperation(function () use (
            $result,
            &$errorCode,
            &$dn,
            &$errorMessage,
            &$referrals,
            &$serverControls
        ) {
            return empty($serverControls)
                ? ldap_parse_result($this->connection, $result, $errorCode, $dn, $errorMessage, $referrals)
                : ldap_parse_result($this->connection, $result, $errorCode, $dn, $errorMessage, $referrals, $serverControls);
        });
    }

    /**
     * @inheritdoc
     */
    public function bind($username, $password)
    {
        return $this->bound = $this->executeFailableOperation(function () use ($username, $password) {
            return ldap_bind($this->connection, $username, $password ? html_entity_decode($password) : null);
        });
    }

    /**
     * @inheritdoc
     */
    public function add($dn, array $entry)
    {
        return $this->executeFailableOperation(function () use ($dn, $entry) {
            return ldap_add($this->connection, $dn, $entry);
        });
    }

    /**
     * @inheritdoc
     */
    public function delete($dn)
    {
        return $this->executeFailableOperation(function () use ($dn) {
            return ldap_delete($this->connection, $dn);
        });
    }

    /**
     * @inheritdoc
     */
    public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
    {
        return $this->executeFailableOperation(function () use (
            $dn,
            $newRdn,
            $newParent,
            $deleteOldRdn
        ) {
            return ldap_rename($this->connection, $dn, $newRdn, $newParent, $deleteOldRdn);
        });
    }

    /**
     * @inheritdoc
     */
    public function modify($dn, array $entry)
    {
        return $this->executeFailableOperation(function () use ($dn, $entry) {
            return ldap_modify($this->connection, $dn, $entry);
        });
    }

    /**
     * @inheritdoc
     */
    public function modifyBatch($dn, array $values)
    {
        return $this->executeFailableOperation(function () use ($dn, $values) {
            return ldap_modify_batch($this->connection, $dn, $values);
        });
    }

    /**
     * @inheritdoc
     */
    public function modAdd($dn, array $entry)
    {
        return $this->executeFailableOperation(function () use ($dn, $entry) {
            return ldap_mod_add($this->connection, $dn, $entry);
        });
    }

    /**
     * @inheritdoc
     */
    public function modReplace($dn, array $entry)
    {
        return $this->executeFailableOperation(function () use ($dn, $entry) {
            return ldap_mod_replace($this->connection, $dn, $entry);
        });
    }

    /**
     * @inheritdoc
     */
    public function modDelete($dn, array $entry)
    {
        return $this->executeFailableOperation(function () use ($dn, $entry) {
            return ldap_mod_del($this->connection, $dn, $entry);
        });
    }

    /**
     * @inheritdoc
     */
    public function controlPagedResult($pageSize = 1000, $isCritical = false, $cookie = '')
    {
        return $this->executeFailableOperation(function () use ($pageSize, $isCritical, $cookie) {
            return ldap_control_paged_result($this->connection, $pageSize, $isCritical, $cookie);
        });
    }

    /**
     * @inheritdoc
     */
    public function controlPagedResultResponse($result, &$cookie, &$estimated = null)
    {
        return $this->executeFailableOperation(function () use ($result, &$cookie, &$estimated) {
            return ldap_control_paged_result_response($this->connection, $result, $cookie, $estimated);
        });
    }

    /**
     * @inheritdoc
     */
    public function freeResult($result)
    {
        return ldap_free_result($result);
    }

    /**
     * @inheritdoc
     */
    public function errNo()
    {
        return $this->connection ? ldap_errno($this->connection) : null;
    }

    /**
     * @inheritdoc
     */
    public function err2Str($number)
    {
        return ldap_err2str($number);
    }

    /**
     * Returns the extended error hex code of the last command.
     *
     * @return string|null
     */
    public function getExtendedErrorHex()
    {
        if (preg_match("/(?<=data\s).*?(?=,)/", $this->getExtendedError(), $code)) {
            return $code[0];
        }
    }

    /**
     * Returns the extended error code of the last command.
     *
     * @return bool|string
     */
    public function getExtendedErrorCode()
    {
        return $this->extractDiagnosticCode($this->getExtendedError());
    }

    /**
     * Extract the diagnostic code from the message.
     *
     * @param string $message
     *
     * @return string|bool
     */
    public function extractDiagnosticCode($message)
    {
        preg_match('/^([\da-fA-F]+):/', $message, $matches);

        return isset($matches[1]) ? $matches[1] : false;
    }

    /**
     * @inheritdoc
     */
    public function getDiagnosticMessage()
    {
        $this->getOption(LDAP_OPT_ERROR_STRING, $message);

        return $message;
    }
}
