<?php

namespace Velis\Bpm;

use ArrayObject;
use Collator;
use Exception;
use ReflectionException;
use User\User as SystemUser;
use Velis\App;
use Velis\Arrays;
use Velis\Bpm\Company\Address;
use Velis\Model\DataObject;
use Velis\Model\DataObject\NameTrait;
use Velis\Model\Hierarchical;
use Velis\Model\Routable;
use Velis\Model\Sanitizable;
use Velis\Notification\Recipient;
use Velis\Notification\SmsRecipient;
use Velis\PersonInterface;
use Velis\User\AnonymizeTrait;

/**
 * Person model
 *
 * @author Bartosz Izdebski <bartosz.izdebski@velis.pl>
 * @author Olek Procki <olo@velis.pl>
 */
class Person extends Hierarchical implements Recipient, Sanitizable, Routable, PersonInterface, SmsRecipient
{
    use AnonymizeTrait;
    use NameTrait;


    /**
     * Buffer for not cached model type (must be redeclared in Cachable class)
     * @var Hierarchical[]
     */
    protected static $_source;


    /**
     * List sort options
     */
    const ORDER_NAME    = 'last_name, first_name';
    const ORDER_COMPANY = 'COALESCE(company_short_name, company_name), last_name, first_name';
    const ORDER_ID      = 'person_id DESC';

    const FITLER_USER   = '\Velis\User';
    const FILTER_PERSON = '\Velis\Bpm\Person';
    const FILTER_ALL    = '\Velis\PersonInterface';


    /**
     * DataStore limited set of fields (used with \Velis\Dojo\Data)
     * @var string[]
     */
    protected static $_dataStoreItemFields = [
        'person_id',
        'user_id',
        'first_name',
        'last_name',
        'full_name',
        'company_id',
        'company_name',
        'company_short_name',
        'search_name',
        'address_no',
    ];


    protected static $_listDefaultOrder = self::ORDER_NAME;
    protected static $_filterListParams = true;


    /**
     * @var Collator
     */
    protected static $_collator;


    /**
     * Person's user account
     * @var User
     */
    protected $_user;


    /**
     * All person's addresses (inactive included)
     * @var Address[]
     */
    protected $_addresses;


    /**
     * Person company data
     * @var Company
     */
    protected $_company;


    /**
     * {@inheritDoc}
     */
    protected function _getTableName()
    {
        return 'app.person';
    }


    /**
     * {@inheritDoc}
     */
    protected function _getListDatasource()
    {
        return 'app.person p';
    }


    /**
     * {@inheritDoc}
     */
    protected function _getPrimaryKeySeq()
    {
        return 'app.person_tab_person_id_seq';
    }


    /**
     * {@inheritDoc}
     */
    public function getParentId()
    {
        return $this->principal_person_id;
    }


    /**
     * {@inheritDoc}
     */
    public function getName()
    {
        return $this['first_name'];
    }


    /**
     * {@inheritDoc}
     */
    public function getLastName()
    {
        return $this['last_name'];
    }


    /**
     * Return name for tree hierarchy
     * @return string
     */
    public function getTreeName()
    {
        $treeName = $this->first_name . ' ' . $this->last_name;
        if ($this['position']) {
            $treeName .= ' (' . $this['position'] . ')';
        }

        return $treeName;
    }


    /**
     * Returns short user name
     *
     * @param bool $reversed
     * @return string
     */
    public function getShortName($reversed = false)
    {
        if ($reversed) {
            return $this->getLastName() . ' ' . mb_substr($this->getName(), 0, 1) . '.';
        } else {
            return mb_substr($this->getName(), 0, 1) . '.' . $this->getLastName();
        }
    }


    /**
     * Returns company if available
     * @param bool $fullInstance
     * @return Company|void
     */
    public function getCompany($fullInstance = false)
    {
        if ($this->company_id) {
            if (isset($this->_company)) {
                return $this->_company;
            } else {
                if (!$fullInstance) {
                    return new \Company\Company($this->company_id);
                } else {
                    return $this->_company = \Company\Company::instance($this->company_id);
                }
            }
        }
    }


    /**
     * Loads company data for persons collection
     * @param Person[] $persons
     * @return \Company\Company[]
     * @throws \Velis\Exception
     */
    public static function loadPersonsCompanies($persons)
    {
        $companies = [];

        if (Arrays::hasColumn($persons, 'company_id')) {
            $companies = \Company\Company::instance(Arrays::getColumn($persons, 'company_id'));

            foreach ($persons as $person) {
                if (array_key_exists($person->company_id, $companies)) {
                    $person->_company = $companies[$person->company_id];
                }
            }
        }

        return $companies;
    }


    /**
     * Returns true if person is related with any company
     * @return bool
     */
    public function hasCompany()
    {
        return $this->company_id > 0;
    }


    /**
     * Returns true if logged user can edit person data
     * @return bool
     */
    public function isEditable()
    {
        if ($this->user_id) {
            if (App::$user->hasPriv('Admin', 'UserEdit')) {
                return true;
            }
            if ($this->company_id && App::$user->hasPriv('Company', 'UserEdit')) {
                return true;
            }
        } else {
            return App::$user->hasPriv('Company', 'PersonEdit');
        }
        return false;
    }


    /**
     * Returns object string representation
     * @see \Velis\Bpm\Person::getFullName()
     *
     * @return string
     */
    public function __toString()
    {
        if ($this->offsetExists('level')) {
            return $this->output(true);
        } else {
            return $this->getFullName(true);
        }
    }


    /**
     * Converts to string
     *
     * @param bool $html use html special chars
     * @return string
     */
    public function output($html = false)
    {
        if ($html) {
            return str_repeat("&nbsp;&nbsp;&nbsp;", $this['level']) . '|&#8212;' . $this->getFullName();
        } else {
            return '|-' . str_repeat("--", $this['level']) . $this->getFullName();
        }
    }


    /**
     * @param DataObject $other
     * @return bool
     */
    public function less(DataObject $other)
    {
        return 0 > $this->compareTo($other);
    }


    /**
     * @param DataObject $other
     * @return bool
     */
    public function greater(DataObject $other)
    {
        return 0 < $this->compareTo($other);
    }


    /**
     * @param DataObject $other
     * @return bool
     */
    public function equals(DataObject $other)
    {
        return 0 == $this->compareTo($other);
    }


    /**
     * Compares object with another instance
     *
     * @param DataObject $other
     * @return int
     */
    public function compareTo(DataObject $other)
    {
        if ($other instanceof PersonInterface) {
            $otherName = $other->getFullName(true);
        } else {
            $otherName = (string) $other;
        }

        $collator = self::_getCollator();

        return $collator->compare($this->getFullName(true), $otherName);
    }


    /**
     * @return Collator
     */
    protected static function _getCollator()
    {
        if (!self::$_collator) {
            self::$_collator = new Collator(setlocale(LC_COLLATE, '0'));
        }

        return self::$_collator;
    }


    /**
     * Returns person email
     * @return string
     */
    public function getEmail()
    {
        return $this->email_address;
    }


    /**
     * Returns person mobile phone
     * @return string
     */
    public function getMobile()
    {
        return $this->phone_no;
    }


    /**
     * Returns rewrite route name
     * @return string
     */
    public function getRouteName()
    {
        return 'person';
    }


    /**
     * Returns standard url
     * @return string
     */
    public function getStandardUrl()
    {
        return '/company/person?person_id=' . $this->id();
    }


    /**
     * Returns edit page url
     * @return string
     */
    public function getEditUrl()
    {
        return App::getRouteUrl('person-edit', $this->_getPrimaryKeyParam());
    }


    /**
     * Returns related user account
     * @return SystemUser
     */
    public function getUser()
    {
        if ($this->is_user) {
            if (SystemUser::isCacheable()) {
                $this->_user = SystemUser::get($this->id());
            } else {
                $this->_user = SystemUser::instance($this->id());
            }
        }

        return $this->_user;
    }


    /**
     * Loads items' user account info
     * @param Person[] $persons
     * @return Person[]
     */
    public static function loadItemsUserData($persons)
    {
        $users = SystemUser::listById(self::getCollectionIds($persons));
        foreach ($persons as $person) {
            if ($users[$person->id()]) {
                $person->_user = $users[$person->id()];
            }
        }

        return $persons;
    }


    /**
     * {@inheritDoc}
     */
    public static function getList($page = 1, $params = null, $order = self::ORDER_NAME, $limit = self::ITEMS_PER_PAGE, $fields = null)
    {
        if ($params['search']) {
            self::$_listConditions[] = "(first_name ILIKE :search OR last_name ILIKE :search)";
            self::$_listParams['search'] = '%' . trim($params['search'], '%') . '%';
        }

        if ($params['group_id']) {
            self::$_listConditions[] = "EXISTS (
                SELECT 1 FROM app.person_group_person_tab pgp
                WHERE pgp.person_group_id = :group_id
                  AND pgp.person_id = p.person_id
            )";

            self::$_listParams['group_id'] = $params['group_id'];
        }

        $fullStrSearch = array (
            'first_name'   => 'first_name',
            'last_name'    => 'last_name',
            'email'        => 'email_address',
            'phone'        => 'phone_no',
            'cellphone'    => 'mobile_phone_no',
            'company_name' => 'company_name'
        );

        foreach ($fullStrSearch as $searchParam => $field) {
            if ($params[$searchParam]) {
                self::$_listConditions[] = $field . ' ILIKE :' . $searchParam;
                self::$_listParams[$searchParam] = '%' . trim($params[$searchParam], '%') . '%';
                unset($params[$searchParam]);
            }
        }

        if ($params['email_address']) {
            self::$_listConditions[] = 'TRIM(email_address) ILIKE TRIM(:email_address)';
            self::$_listParams['email_address'] = $params['email_address'];
            unset($params['email_address']);
        }

        if ($params['company_type_id']) {
            if (!is_array($params['company_type_id'])) {
                $params['company_type_id'] = array($params['company_type_id']);
            }

            self::$_listConditions[] = "EXISTS(
                SELECT 1 FROM
                  app.person_to_company_address_tab ptca
                  JOIN app.company_tab c ON ptca.company_id = c.company_id
                  JOIN app.company_to_company_type_tab ctct ON ctct.company_id = c.company_id

                WHERE ctct.company_type_id IN('" . implode("','", $params['company_type_id']) . "')
                  AND ptca.person_id = p.person_id
                  AND ptca.active = 1
            )";
        }

        if ($params['event_ticket_id']) {
            self::$_listConditions[] = "EXISTS (
                    SELECT 1 FROM app.event_ticket_location_person_tab etlp
                    WHERE etlp.person_id = p.person_id
                      AND etlp.event_ticket_id = :event_ticket_id
                )";
            self::$_listParams['event_ticket_id'] = $params['event_ticket_id'];
        }

        if ($params['mailing_id']) {
            self::$_listConditions[] = "EXISTS (
                    SELECT 1 FROM app.mailing_person_tab mp
                    WHERE mp.person_id = p.person_id
                      AND mp.mailing_id = :mailing_id
                )";
            self::$_listParams['mailing_id'] = $params['mailing_id'];
        }

        if (isset($params['is_bot'])) {
            if ($params['is_bot']) {
                self::$_listConditions[] = "EXISTS(
                  SELECT 1 FROM acl.user u WHERE u.user_id = p.user_id AND u.is_bot = 1
                )";
            } else {
                self::$_listConditions[] = "NOT EXISTS(
                  SELECT 1 FROM acl.user u WHERE u.user_id = p.user_id AND u.is_bot = 1
                )";
            }
            unset($params['is_bot']);
        }

        if ($params['for_mailing_id']) {
            self::$_listConditions[] = "NOT EXISTS (
                SELECT 1 FROM app.mailing_person_tab mp
                WHERE mp.person_id = p.person_id
                  AND mp.mailing_id = :for_mailing_id
            )";
            self::$_listParams['for_mailing_id'] = $params['for_mailing_id'];
        }

        if ($params['root_only']) {
            self::$_listConditions[] = "principal_person_id IS NULL";
        }

        if ($params['app_company']) {
            self::$_listConditions[] = "EXISTS(
                SELECT 1 FROM
                  app.person_to_company_address_tab ptca
                  JOIN app.company_tab c ON ptca.company_id = c.company_id
                WHERE ptca.person_id = p.person_id AND c.company_id = :app_company_id)";
            self::$_listParams['app_company_id'] = $params['app_company'];
        }

        return parent::getList($page, $params, $order, $limit, $fields);
    }


    /**
     * Finds person by email address, returns first result
     *
     * @param string $email Email address
     * @return Person
     * @throws \Velis\Exception
     */
    public static function byEmail($email)
    {
        return Arrays::getFirst(
            self::getList(null, array('email_address' => $email), null, 1)
        );
    }


    /**
     * @param Company $company
     * @param array|ArrayObject $additionalParams
     * @return Person[]
     * @throws ReflectionException
     */
    public static function byCompany($company, $additionalParams = null)
    {
        $params = [
            'company_id' => $company instanceof Company ? $company->id() : $company,
        ];

        if ($additionalParams) {
            $params = array_merge($params, $additionalParams);
        }

        return self::listAll($params);
    }


    /**
     * Returns person address in company
     * @return Address|void
     */
    public function getAddress()
    {
        if ($this->company_id && $this->address_no) {
            foreach ($this->getAddressList() as $address) {
                if (
                    $address->company_id == $this->company_id
                    && $address->address_no == $this->address_no
                ) {
                    return $address;
                }
            }
        }
    }


    /**
     * Returns person assignment address in company
     * @return Address[]
     */
    public function getAddressList()
    {
        if (!isset($this->_addresses)) {
            $result = self::$_db->getAll(
                "SELECT COALESCE(c.short_name,c.name) AS company_name,
                        ca.*,
                        ptca.active
                   FROM app.person_to_company_address_tab ptca
                   JOIN app.company_address_tab ca USING(company_id, address_no)
                   JOIN app.company_tab c USING(company_id)
                 WHERE person_id = :person_id",
                $this->_getPrimaryKeyParam()
            );

            $this->_addresses = array();

            foreach ($result as $row) {
                if (class_exists('\Company\Address')) {
                    $this->_addresses[] = new \Company\Address($row);
                } else {
                    $this->_addresses[] = new Address($row);
                }
            }
        }

        return $this->_addresses;
    }


    /**
     * Sets address in company to person
     *
     * @param int $companyId
     * @param int $addressNo
     * @return $this
     * @throws Exception
     */
    public function setAddress($companyId, $addressNo)
    {
        $commit = self::$_db->startTrans();

        try {
            // only one address is allowed for single company
            $this->_removeAddress($companyId);

            // only one active address is permitted for person
            self::$_db->execDML(
                "UPDATE app.person_to_company_address_tab SET active=0 WHERE person_id=:person_id",
                $this->_getPrimaryKeyParam()
            );

            self::$_db->insert(
                'app.person_to_company_address_tab',
                array(
                    'person_id' => $this->id(),
                    'company_id' => $companyId,
                    'address_no' => $addressNo,
                    'active' => 1
                )
            );

            $this['company_id'] = $companyId;
            $this['address_no'] = $addressNo;

            if ($commit) {
                self::$_db->commit();
            }

            return $this;
        } catch (Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }
            throw $e;
        }
    }


    /**
     * Removes person's company address
     *
     * @param \Company\Company|int $company
     * @return $this
     */
    protected function _removeAddress($company)
    {
        self::$_db->execDML(
            "DELETE FROM app.person_to_company_address_tab ptca
             WHERE person_id = :person_id AND company_id = :company_id",
            [
                'person_id' => $this->id(),
                'company_id' => $company instanceof Company ? $company->id() : $company,
            ]
        );

        return $this;
    }


    /**
     * Deactivates person's company address
     *
     * @param \Company\Company|int $company
     * @param int $addressNo
     * @return $this
     * @throws Exception
     */
    public function deactivateAddress($company, $addressNo)
    {
        $commit = self::$_db->startTrans();

        try {
            self::$_db->update(
                'app.person_to_company_address_tab',
                array('active' => null),
                array(
                    'person_id' => $this->id(),
                    'company_id' => $company instanceof Company ? $company->id() : $company,
                    'address_no' => $addressNo
                )
            );

            // non internal users must have active company relation for application login
            if ($user = $this->getUser()) {
                $user['enabled'] = 0;
                $user->modify();
            }

            unset(
                $this['company_id'],
                $this['address_no']
            );

            if ($commit) {
                self::$_db->commit();
            }

            return $this;
        } catch (Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }
            throw $e;
        }
    }


    /**
     * Activates person's company address
     *
     * @param \Company\Company|int $company
     * @param int $addressNo
     * @return $this
     */
    public function activateAddress($company, $addressNo)
    {
        $params = [
            'person_id' => $this->id(),
            'company_id' => $company instanceof Company ? $company->id() : $company,
            'address_no' => $addressNo,
        ];

        self::$_db->update(
            'app.person_to_company_address_tab',
            array('active' => 1),
            $params
        );

        $this['company_id'] = $params['company_id'];
        $this['address_no'] = $params['address_no'];

        return $this;
    }


    /**
     * Get avatar
     * @return string
     */
    public function getAvatar()
    {
        return '/res/img/layout/default-avatar.png';
    }
}
