<?php

namespace Velis\Bpm;

use Velis\Model\Hierarchical;
use Velis\Model\Cacheable;
use Velis\Bpm\Ticket\Category;
use Velis\App;
use Velis\Arrays;
use Velis\Acl;
use Admin\ExternalSiteLogin;
use Velis\VersionControl\Svn\Repository;
use User\User as SystemUser;

/**
 * Project model
 *
 * @author Bartosz Izdebski <bartosz.izdebski@velis.pl>
 * @author Olek Procki <olo@velis.pl>
 */
class Project extends Hierarchical implements Cacheable
{

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


    /**
     * Category matrix class
     * @var string
     */
    protected static $_matrixClass = '\Velis\Bpm\Ticket\CategoryMatrix';


    /**
     * companyList list
     * @var \Velis\Bpm\Company[]
     */
    protected $_companyList;


    /**
     * Attached ticket categories
     * @var array
     */
    protected $_categories;


    /**
     * Cached involved users list
     * @var \User\User[]
     */
    protected $_involvedUsers;


    /**
     * Involved person list
     * @var \Velis\PersonInterface[]
     */
    protected $_involvedPersons;


    /**
     * Project selectable categories
     * @var \Velis\Bpm\Ticket\Category[]
     */
    protected $_selectableCategories;


    /**
     * Project observers
     * @var \User\User[]
     */
    protected $_observers;


    /**
     * Returns related sql table
     * @return string
     */
    protected function _getTableName()
    {
        return 'app.project_tab';
    }

    /**
     * Returns datasource for listing function
     * @return string
     */
    protected function _getListDatasource()
    {
        return 'app.project p';
    }


    /**
     * Adds new project
     * @param \Velis\Bpm\Ticket\Category[] $categories
     */
    public function add($categories = null)
    {
        parent::add(true);

        if ($categories != null) {
            foreach ($categories as $category) {
                $this->addCategory($category);
            }
        }
        return $this;
    }


    /**
     * Modifies project
     * @param \Velis\Bpm\Ticket\Category[] $categories
     */
    public function modify($categories = null)
    {
        parent::modify();

        if (is_array($categories)) {
            $this->removeCategories();
            foreach ($categories as $category) {
                $this->addCategory($category);
            }
        }
        return $this;
    }


    /**
     * Returns project list
     *
     * @param int $page
     * @param array|ArrayObject $params
     * @param string $order
     * @param int $limit
     * @param string|array $fields
     *
     * @return \Velis\Bpm\Ticket\Ticket[]
     */
    public static function getList($page = 1, $params = null, $order = null, $limit = self::ITEMS_PER_PAGE, $fields = null)
    {
        self::$_listConditions = array();

        $company = new \Company\Company();
        $companyField = $company->_getPrimaryKeyField();

        if ($params[$companyField]) {
            self::$_listConditions[] = "EXISTS (
                SELECT 1 FROM app.project_" . str_replace('_id', '_tab', $companyField) . " pc
                  WHERE pc.project_id = p.project_id
                    AND pc.$companyField=:$companyField
            )";
            self::$_listParams[$companyField] = $params[$companyField];
        }

        if ($params['hasWiki']) {
            self::$_listConditions[] = "wiki_page_id IS NOT NULL";
        }

        if ($params['projectReminder']) {
             self::$_listConditions[] = "support_end_date IS NOT NULL AND (support_end_date - interval '15day' = CURRENT_DATE OR support_end_date - interval '30day' = CURRENT_DATE)";
        }
        $extParams = Arrays::extractFields(
            $params,
            array(
                'project_id',
                'acro',
                'name',
                'notes',
                'parent_project_id',
                'date_added',
                'date_last_modified',
                'added_user_id',
                'repository_id'
            )
        );
        return parent::getList($page, array_filter($extParams), $order, $limit, $fields);
    }


    /**
     * Return project cutomers' ids
     *
     * @return int[]
     */
    public function getCompanyIds()
    {
        if ($this->getCompanyList()) {
            return \Company\Company::getCollectionIds($this->getCompanyList());
        } else {
            return array();
        }
    }


    /**
     * Get all companyList
     * @return \Velis\Bpm\Company[]
     */
    public function getCompanyList()
    {
        if (!$this->id()) {
            return array();
        }
        if (!isset($this->_companyList)) {
            $this->_companyList = \Company\Company::listAll(array('project_id' => $this->id()));
        }
        return $this->_companyList;
    }


    /**
     * Returns true if company's involved in project
     *
     * @param \Velis\Bpm\Company|int $company
     * @return bool
     */
    public function hasCompany($company)
    {
        if (!$company instanceof Company) {
            $company = new \Company\Company($company);
        }
        return $company->belongs($this->getCompanyList());
    }


    /**
     * Remove all companyList
     *
     */
    public function removeCompanies()
    {
        $company = new \Company\Company();
        $companyField = $company->_getPrimaryKeyField();

        self::$_db->execDML(
            "DELETE FROM app.project_" . str_replace('_id', '_tab', $companyField) . "
             WHERE project_id = :project_id",
            $this->_getPrimaryKeyParam()
        );
    }


    /**
     * Adds company to project
     *
     * @param \Velis\Bpm\Company|int $company
     * @return \Velis\Bpm\Project
     */
    public function addCompany($company)
    {
        return $this->addCompanies(array($company));
    }


    /**
     * Add companyList
     *
     * @param \Velis\Bpm\Company[]|int[] $companyList
     * @return \Velis\Bpm\Project
     */
    public function addCompanies(array $companyList)
    {
        $companyInstance = new \Company\Company();
        $companyField = $companyInstance->_getPrimaryKeyField();

        foreach ($companyList as $company) {
            $params = array(
                'project_id'  => $this->id(),
                $companyField => $company instanceof Company ? $company->id() : $company,
            );
            self::$_db->insert('app.project_' . str_replace('_id', '_tab', $companyField), $params);
        }
        return $this;
    }


    /**
     * Remove company
     * @return int
     */
    public function removeCompany($companyId)
    {
        $company = new \Company\Company();
        $companyField = $company->_getPrimaryKeyField();

        return self::$_db->execDML(
            "DELETE FROM app.project_" . str_replace('_id', '_tab', $companyField) . " WHERE project_id = :project_id AND $companyField = :$companyField",
            array (
                'project_id'  => $this->id(),
                $companyField => intval($companyId)
            )
        );
    }


    /**
     * Returns project parent id
     * @return int
     */
    public function getParentId()
    {
        return $this['parent_project_id'];
    }


    /**
     * Loads projects companyList at once
     * @param \Velis\Bpm\Project[] $projects
     */
    public static function loadProjectsCompanyList($projects)
    {
        $projectsIds = self::getCollectionIds($projects);

        $company = new \Company\Company();
        $companyField = $company->_getPrimaryKeyField();

        if (count($projectsIds)) {
            $query = "SELECT c.* FROM app.project_" . str_replace('_id', '_tab', $companyField) . " pc
                        JOIN " . str_replace('_tab', '', $company->_getTableName()) . " c USING($companyField)
                        WHERE project_id IN(" . implode(", ", $projectsIds) . ")";

            foreach ($projects as $project) {
                $project->_companyList = array();
            }

            foreach (self::$_db->getAll($query) as $row) {
                if ($projects[$row['project_id']]) {
                    $project = $projects[$row['project_id']];
                    $project->_companyList[$row[$companyField]] = new Company($row[$companyField]);
                }
            }
        }
    }


    /**
     * Returns related categories
     * @return \Velis\Bpm\Ticket\Category[]
     */
    public function getCategories()
    {
        if (!isset($this->_categories)) {
            $this->_categories = array();

            $query  = "SELECT ticket_category_id FROM app.project_ticket_category_tab WHERE project_id=:project_id";
            $params = array('project_id' => $this->id());

            $result = self::$_db->getAll($query, $params, 'ticket_category_id');
            $this->_categories = Category::get(array_keys($result));
        }
        return $this->_categories;
    }


    /**
     * Returns selectable categories (subcategories by recursion)
     * @return \Velis\Bpm\Ticket\Category[]
     */
    public function getSelectableCategories($output = false, $currentCategory = null)
    {
        if (!isset($this->_selectableCategories)) {
            $cacheKey = 'project' . $this->id() . '_selectable_categories';
            $this->_selectableCategories = App::$cache[$cacheKey];

            if (!is_array($this->_selectableCategories)) {
                $categories = array();
                foreach ($this->getCategories() as $category) {
                    if ($category->isActive() || $currentCategory && $currentCategory == $category->ticket_category_id) {
                        $categories[] = $category;
                    }
                }
                $this->_selectableCategories = Category::getNestedList(
                    array('active' => 1),
                    $categories
                );
                App::$cache[$cacheKey] = $this->_selectableCategories;
            }
        }

        if ($output) {
            foreach ($this->_selectableCategories as $categoryId => $category) {
                $category['output'] = $category->output();
            }
        }

        return $this->_selectableCategories;
    }


    /**
     * Loads projects categories at once
     * @param \Velis\Bpm\Project[] $projects
     */
    public static function loadProjectsCategories($projects)
    {
        $projectsIds = self::getCollectionIds($projects);

        if (count($projectsIds)) {
            $query = "SELECT * FROM app.project_ticket_category_tab WHERE project_id IN(" . implode(", ", $projectsIds) . ")";

            foreach ($projects as $project) {
                $project->_categories = array();
            }

            foreach (self::$_db->getAll($query) as $row) {
                if ($projects[$row['project_id']]) {
                    $project = $projects[$row['project_id']];
                    $project->_categories[$row['ticket_category_id']] = Category::get($row['ticket_category_id']);
                }
            }
        }
    }


    /**
     * Returns true if project has category attached
     *
     * @param \Velis\Bpm\Ticket\Category|int $category
     * @return bool
     */
    public function hasCategory($category)
    {
        $categoryId = $category instanceof Category ? $category->id() : $category;
        return array_key_exists($categoryId, $this->getCategories());
    }


    /**
     * Adds category to project
     * @param \Velis\Bpm\Ticket\Category|int $category
     *
     * @throws \LogicException when category or one of its childres attached already
     */
    public function addCategory($category)
    {
        try {
            $params = [
                'project_id' => $this->id(),
                'ticket_category_id' => $category instanceof Category ? $category->id() : $category
            ];
            self::$_db->insert('app.project_ticket_category_tab', $params);
            unset(App::$cache['project' . $this->id() . '_selectable_categories']);
        } catch (Exeption $e) {
            if ($e->getCode() == 23505) {
                throw new \LogicException('Category already added');
            }
            throw $e;
        }
    }


    /**
     * Removes ticket category
     * @param \Velis\Bpm\Ticket\Category|int $category
     *
     * @throws \LogicException
     */
    public function removeCategory($category)
    {
        if (count($this->getCategories()) <= 1) {
            throw new \LogicException('Projekt powinien posiadać przynajmniej jedną kategorię');
        }

        $params = array (
            'project_id' => $this->id(),
            'ticket_category_id' => $category instanceof Category ? $category->id() : $category
        );

        self::$_db->startTrans();

        // delete all project assignments from category matrix
        self::$_db->execDML(
            "DELETE FROM app.ticket_category_user_matrix_tab
                             WHERE project_id=:project_id
                               AND ticket_category_id=:ticket_category_id",
            $params
        );

        // delete category from project categories
        self::$_db->execDML(
            "DELETE FROM app.project_ticket_category_tab
                             WHERE project_id=:project_id
                               AND ticket_category_id=:ticket_category_id",
            $params
        );

        self::$_db->commit();

        unset(App::$cache['project' . $this->id() . '_selectable_categories']);
    }


    /**
     * Removes all attached categories
     * @return \Velis\Bpm\Project
     */
    public function removeCategories()
    {
        self::$_db->execDML(
            "DELETE FROM app.project_ticket_category_tab WHERE project_id = :project_id",
            array('project_id' => $this->id())
        );

        unset(App::$cache['project' . $this->id() . '_selectable_categories']);
        return $this;
    }


    /**
     * Returns true if project is active
     * @return bool
     */
    public function isActive()
    {
        return $this['active'] == 1;
    }


    /**
     * Returns projects list with nested order (depth info included as 'level' field)
     *
     * @param array $filter
     * @param array $predefinedList
     *
     * @return Project[]
     */
    public static function getNestedList(array $filter = null, array $predefinedList = null)
    {
        if (null === $filter) {
            $filter = ['active' => 1];
        }

        return parent::getNestedList($filter, $predefinedList);
    }


   /**
     * Returns category operators
     *
     * @param array $params
     * @return \User\User[]
     */
    public function getOperators(array $params = array())
    {
        $params = array_merge($params, array('project_id' => $this->id()));
        $matrixClass = static::$_matrixClass;

        $matrix = $matrixClass::listAll($params);

        $operators = array();

        foreach ($matrix as $elem) {
            $operator = $elem->getOperator();
            if (!$operator->belongs($operators) && $operator->isActive()) {
                $operators[] = $operator;
            }
        }
        if (empty($operators)) {
            if ($this->isMainElement()) {
                $category = Category::get($params['ticket_category_id']);
                $params['any_project'] = true;

                return $category->getOperators($params);
            } else {
                return $this->getParent()->getOperators($params);
            }
        }
        return $operators;
    }


    /**
     * Returns application level matrix class for projects
     * @return string
     */
    public function getMatrixClass()
    {
        return static::$_matrixClass;
    }


    /**
     * Lists users that can be added to the ticket as observers
     *
     * @todo drop cache access
     *
     * @param bool $reload forces list reload
     * @return User\User[]
     */
    public function getInvolvedUsers($reload = false)
    {
        if (!isset($this->_involvedUsers) || $reload) {
            $users = SystemUser::listCached();

            foreach ($users as $key => $user) {
                if (!$user->isActive() || ($user->hasCompany() && !$this->hasCompany($user->getCompanyId()))) {
                    unset($users[$key]);
                }
            }

            $users = array_values($users);

            $this->_involvedUsers = $users;
        }
        return $this->_involvedUsers;
    }


    /**
     * Lists persons that can be added to the ticket as observers
     *
     * @param bool $reload forces list reload
     * @return \Velis\PersonInterface[]
     */
    public function getInvolvedPersons($order = null)
    {
        if (!isset($this->_involvedPersons)) {
            $this->_involvedPersons = array();

            foreach (SystemUser::listServicemen() as $user) {
                $user['person_id'] = $user->id();
                $this->_involvedPersons[] = $user;
            }

            if ($this->getCompanyIds()) {
                $persons = Person::listAll(
                    array('company_id' => $this->getCompanyIds()),
                    $order
                );
                foreach ($persons as $person) {
                    $this->_involvedPersons[] = $person;
                }
            }
        }
        return $this->_involvedPersons;
    }


    /**
     * List users priviledged for accepting billable hours
     * @return \User\User[]
     */
    public function getBillableHoursAccepters()
    {
        $accepters = array();

        foreach (Acl::getPriviledgedUsers('Ticket', 'AddAccept') as $user) {
            if ($user->getCompanyId() && $this->hasCompany($user->getCompanyId()) && $user->isActive()) {
                $accepters[$user->id()] = $user;
            }
        }
        return $accepters;
    }


    /**
     * Observers list
     *
     * @param bool $includeParentProjects
     * @return \User\User[]
     */
    public function getObservers($includeParentProjects = false)
    {
        if ($includeParentProjects && $this['path']) {
            static $allObservers = null;

            if (!is_array($allObservers)) {
                $allObservers = [];
                $params['project_id'] = explode(',', trim($this['path'], '{}'));
                $sql  = 'SELECT DISTINCT u.* FROM acl.user u JOIN app.project_observer_tab po USING(user_id) WHERE 1=1 ' . self::$_db->conditions($params);
                $rows = self::$_db->getAll($sql, $params);

                foreach ($rows as $row) {
                    $allObservers[$row['user_id']] = new SystemUser($row['user_id']);
                }
            }
            return $allObservers;
        }
        if (!isset($this->_observers)) {
            $users = [];
            $rows = self::$_db->getAll(
                'SELECT u.* FROM acl.user u JOIN app.project_observer_tab po USING(user_id) WHERE po.project_id = :project_id',
                ['project_id' => $this->id()]
            );
            foreach ($rows as $row) {
                $users[$row['user_id']] = new SystemUser($row['user_id']);
            }
            $this->_observers = $users;
        }

        return $this->_observers;
    }


    /**
     * Remove observer
     * @return int
     */
    public function removeObserver($userId)
    {
        return self::$_db->execDML(
            'DELETE FROM app.project_observer_tab WHERE project_id = :project_id AND user_id = :user_id',
            array (
                'project_id' => $this->id(),
                'user_id' => intval($userId)
            )
        );
    }


    /**
     * Add observer
     * @return PDOStatement|int
     */
    public function addObserver($userId)
    {
        return self::$_db->insert(
            'app.project_observer_tab',
            array (
                'project_id' => $this->id(),
                'user_id' => intval($userId)
            )
        );
    }

   /**
     *
     * @return ExternalSiteLogin[]
     */
    public function getExternalSites()
    {
        $sites = array();

        foreach (ExternalSiteLogin::listActive() as $siteId => $site) {
            $path = explode(',', str_replace(array('{', '}'), '', $this->path));
            if (
                in_array($site['project_id'], $path) ||
                (!empty($site['repository_id']) &&
                    $site['repository_id'] == $this['recursive_repository_id'] &&
                    $site['external_site_login_type_id'] == 'Dev')
            ) {
                if ($site->checkAccess()) {
                    $sites[$siteId] = $site;
                }
            }
        }

        return $sites;
    }

    /**
     *
     * @return Repository
     */
    public function getRepository()
    {
        return Repository::get($this['repository_id']);
    }
}
