<?php

  /**
   * Project class
   *
   * @package activeCollab.modules.system
   * @subpackage models
   */
  class Project extends BaseProject implements IRoutingContext, IConfigContext, IUsersContext, INotifierContext, IComplete, ILabel, ICategoriesContext, ICategory, IHistory, IState, ISearchItem, IAvatar, ICanBeFavorite, ITracking, IInvoiceBasedOn, IActivityLogs, IObjectContext {
    
    // Project stauts filters 
    const STATUS_ANY = 'any';
    const STATUS_ACTIVE = 'active';
    const STATUS_COMPLETED = 'completed';
    
    /**
     * List of rich text fields
     *
     * @var array
     */
    protected $rich_text_fields = array('overview');
    
    /**
     * List of protected fields (can't be set using setAttributes() method)
     *
     * @var array
     */
  	protected $protect = array(
  	  'id',
  	  'completed_on',
  	  'completed_by_id',
  	  'completed_by_name',
  	  'completed_by_email',
  	  'created_on',
  	  'created_by_id',
  	  'created_by_name',
  	  'created_by_email',
  	  'open_tasks_count',
  	  'total_tasks_count'
  	);
  	
  	/**
  	 * Return template that's been used to create this project
  	 * 
  	 * @return Project
  	 */
  	function getTemplate() {
      return DataObjectPool::get('Project', $this->getTemplateId());
  	} // getTemplate
  	
  	/**
  	 * Set template
  	 * 
  	 * @param Project $value
  	 * @param boolean $save
  	 * @return Project
  	 */
  	function setTemplate($value, $save = false) {
  	  if($value instanceof Project) {
  	    $this->setTemplateId($value->getId());
  	  } elseif($value === null) {
  	    $this->setTemplateId(null);
  	  } else {
  	    throw new InvalidInstanceError('value', $value, 'Project');
  	  } // if

      if($save) {
        $this->save();
      } // if

      return $this->getTemplate();
  	} // setTemplate
    
    /**
     * Cached based on instance
     *
     * @var ApplicationObject
     */
    protected $based_on = false;
    
    /**
     * Return parent project that this object is based on
     * 
     * @return IProjectBasedOn
     */
    function getBasedOn() {
      if($this->based_on === false) {
        $based_on_class = $this->getBasedOnType();
        $based_on_id = $this->getBasedOnId();
        
        if($based_on_class && $based_on_id) {
          $this->based_on = new $based_on_class($based_on_id);
          
          if(!($this->based_on instanceof IProjectBasedOn)) {
            $this->based_on = null;
          } // if
        } else {
          $this->based_on = null;
        } // if
      } // if
      
      return $this->based_on;
    } // getBasedOn
    
    /**
     * Set project based on value
     * 
     * @param IProjectBasedOn $value
     * @param boolean $save
     */
    function setBasedOn($value, $save = false) {
      if($value instanceof IProjectBasedOn) {
        $this->setBasedOnType(get_class($value));
        $this->setBasedOnId($value->getId());
      } elseif($value === null) {
        $this->setBasedOnType(null);
        $this->setBasedOnId(null);
      } else {
        throw new InvalidInstanceError('value', $value, 'ApplicationObject');
      } // if
      
      $this->based_on = $value;
      
      if($save) {
        $this->save();
      } // if
      
      return $this->based_on;
    } // setBasedOn
    
    /**
     * Project leader
     *
     * @var User
     */
    private $leader = false;
    
    /**
     * Return project leader
     *
     * @return User
     */
    function getLeader() {
      if($this->leader === false) {
        if($this->getLeaderId()) {
          $this->leader = Users::findById($this->getLeaderId());
        } // if
        
        if(!($this->leader instanceof User)) {
          if($this->getLeaderEmail()) {
            $this->leader = new AnonymousUser($this->getLeaderName(), $this->getLeaderEmail());
          } else {
            $this->leader = null;
          } // if
        } // if
      } // if
      return $this->leader;
    } // getLeader
    
    /**
     * Set leader data
     *
     * @param IUser $leader
     * @return User
     */
    function setLeader(IUser $leader) {
      if($leader instanceof User) {
        $this->setLeaderId($leader->getId());
        $this->setLeaderName($leader->getDisplayName());
        $this->setLeaderEmail($leader->getEmail());
      } else {
        throw new InvalidInstanceError('$leader', $leader, 'User', '$leader is expected to be an instance of User or AnonymousUser class');
      } // if
      $this->leader = false;
      return $leader;
    } // setLeader
    
    /**
     * Returns true if $user is leader of this particular project
     *
     * @param User $user
     * @return boolean
     */
    function isLeader(User $user) {
      return $this->getLeaderId() == $user->getId();
    } // isLeader
    
    /**
     * Return company instance
     *
     * @return Company
     */
    function getCompany() {
      $company_id = $this->getCompanyId();
      
      if($company_id && DataObjectPool::get('Company', $company_id) instanceof Company) {
        return DataObjectPool::get('Company', $company_id);
      } else {
        return Companies::findOwnerCompany();
      } // if
    } // getCompany
    
    /**
     * Return project currency
     * 
     * @return Currency
     */
    function getCurrency() {
      $currency_id = $this->getCurrencyId();
      
      if($currency_id && DataObjectPool::get('Currency', $currency_id) instanceof Currency) {
        return DataObjectPool::get('Currency', $currency_id);
      } else {
        return Currencies::getDefault();;
      } // if
    } // getCurrency
    
    /**
     * Set currency value
     * 
     * $currency can be Currency instance, or NULL. In case of NULL, this 
     * project will use default currency
     * 
     * @param Currency $currency
     * @param boolean $save
     * @return Currency
     */
    function setCurrency($currency, $save = false) {
      if($currency instanceof Currency) {
        $this->setCurrencyId($currency->getId());
      } elseif($currency === null) {
        $this->setCurrencyId(null);
      } else {
        throw new InvalidInstanceError('currency', $currency, 'Currency');
      } // if
      
      if($save) {
        $this->save();
      } // if
      
      return $this->getCurrencyId();
    } // setCurrency
    
    /**
     * Cached cost so far value
     *
     * @var float
     */
    private $cost_so_far = false;
    
    /**
     * Return cost so far
     * 
     * @param IUser $user
     * @return float
     */
    function getCostSoFar(IUser $user) {
      if($this->cost_so_far === false) {
        $this->cost_so_far = TrackingObjects::sumCostByProject($user, $this);
      } // if
      
      return $this->cost_so_far;
    } // getCostSoFar
    
    /**
     * Return cost so far in percent
     * 
     * @param IUser $user
     * @return float
     */
    function getCostSoFarInPercent(IUser $user) {
      if($this->getBudget() > 0) {
        $cost_so_far = $this->getCostSoFar($user);
        
        if($cost_so_far > 0) {
          return ceil(($cost_so_far * 100) / $this->getBudget());
        } else {
          return 0;
        } // if
      } else {
        return null;
      } // if
    } // getCostSoFarInPercent
    
    /**
     * Return verbose status
     *
     * @return string
     */
    function getVerboseStatus() {
      return $this->getCompletedOn() instanceof DateValue ? lang('Completed') : lang('Active');
    } // getVerboseStatus
    
    /**
  	 * Set attributes
  	 * 
  	 * @param array $attributes
  	 */
  	function setAttributes($attributes) {
  		if(isset($attributes['budget'])) {
  			$attributes['budget'] = moneyval($attributes['budget']);
  		} // if
  		
  		parent::setAttributes($attributes);
  	} // setAttributes
    
    /**
     * Prepare list of options that $user can use
     *
     * @param IUser $user
     * @param NamedList $options
     * @param string $interface
     * @return NamedList
     */
    protected function prepareOptionsFor(IUser $user, NamedList $options, $interface = AngieApplication::INTERFACE_DEFAULT) {
      
      // Default interface
      if($interface == AngieApplication::INTERFACE_DEFAULT) {
        parent::prepareOptionsFor($user, $options, $interface);
        
        if($this->canEdit($user)) {
          $options->addAfter('edit_settings', array(
            'url' => $this->getSettingsUrl(),
            'text' => lang('Change Settings'),
            'onclick' => new FlyoutFormCallback('project_settings_updated', array(
              'success_message' => lang('Project settings have been updated'),
            )),
          ), 'edit');
        } // if
        
        if(AngieApplication::isModuleLoaded('invoicing') && $user->isFinancialManager() && $this->tracking()->hasBillable($user,true)) {
          $options->add('make_invoice', array(
            'url' => $this->invoice()->getUrl(),
            'text' => lang('Create Invoice'),
            'onclick' => new FlyoutFormCallback('create_invoice_from_project'), 
          ));
        } // if
        
      // Mobile devices
      } elseif($interface == AngieApplication::INTERFACE_PHONE || $interface == AngieApplication::INTERFACE_TABLET) {
        $this->complete()->prepareObjectOptions($options, $user, $interface);
        $this->state()->prepareObjectOptions($options, $user, $interface);
      } // if
      
      return $options;
    } // prepareOptionsFor
    
    /**
     * Return array or property => value pairs that describes this object
     *
     * $user is an instance of user who requested description - it's used to get
     * only the data this user can see
     *
     * @param IUser $user
     * @param boolean $detailed
     * @param boolean $for_interface
     * @return array
     */
    function describe(IUser $user, $detailed = false, $for_interface = false) {
    	$result = parent::describe($user, $detailed, $for_interface);
      
      $result['icon'] = $this->avatar()->getUrl(IAvatarImplementation::SIZE_SMALL);
      $result['overview'] = $this->getOverview();
      $result['overview_formatted'] = HTML::toRichText($this->getOverview());
      $result['currency_code'] = $this->getCurrency() instanceof Currency ? $this->getCurrency()->getCode() : null;
      $result['based_on'] = $this->getBasedOn();
      $result['status_verbose'] = $this->getVerboseStatus();
      
      $result['progress'] = lang(':completed of :total task completed (:percentage % done)', array('completed' => $this->getCompletedTaskCount(), 'total' => $this->getTotalTasksCount(), 'percentage' => $this->getPercentsDone()));
    
      
      if($user instanceof User && $user->canSeeProjectBudgets()) {
        $result['budget'] = $this->getBudget();
        $result['permissions']['can_see_budget'] = true;
      } else {
        $result['permissions']['can_see_budget'] = false;
      } // if
      
      
      $this->category()->describe($user, false, $for_interface, $result);
      
      if($detailed) {
      	$result['cost_summarized'] = AngieApplication::isModuleLoaded('tracking') ? TrackingObjects::sumCostByProject($user, $this) : 0;
      	
        $result['leader'] = $this->getLeader() instanceof IUser ? $this->getLeader()->describe($user, false, $for_interface) : null;
        $result['company'] = $this->getCompany() instanceof Company ? $this->getCompany()->describe($user, false, $for_interface) : null;
        
        // Progress
        list($total_assignments, $open_assignments) = ProjectProgress::getProjectProgress($this);
        
        $result['total_assignments'] = $total_assignments;
        $result['open_assignments'] = $open_assignments;
        $result['label_id'] = $this->getLabelId();
        
        // Permissions
        $logged_user_permissions = array(
          'role' => null,
          'permissions' => array(),
        );
        
        $permissions = array_keys(ProjectRoles::getPermissions());
        if($user->isAdministrator()) {
          $logged_user_permissions['role'] = 'administrator';
        } elseif($user->isProjectManager()) {
          $logged_user_permissions['role'] = 'project-manager';
        } elseif($this->isLeader($user)) {
          $logged_user_permissions['role'] = 'project-leader';
        } // if
        
        if($logged_user_permissions['role'] === null) {
          $project_role = $user->projects()->getRole($this);
          if($project_role instanceof Role) {
            $logged_user_permissions['role'] = $project_role->getId();
          } else {
            $logged_user_permissions['role'] = 'custom';
          } // if
          
          foreach($permissions as $permission) {
            $logged_user_permissions['permissions'][$permission] = (integer) $user->projects()->getPermission($permission, $this);
          } // foreach
        } else {
          foreach($permissions as $permission) {
            $logged_user_permissions['permissions'][$permission] = ProjectRole::PERMISSION_MANAGE;
          } // foreach
        } // if
        
        $result['logged_user_permissions'] = $logged_user_permissions;
        
        // Additional steps, for interface
        if($for_interface) {
          $result['additional_steps'] = array();
          
          foreach($this->getAdditionalSteps($user) as $step_name => $step_text) {
            $result['additional_steps'][$step_name] = array(
              'text' => $step_text, 
              'url' => $this->getAdditionalStepUrl($step_name), 
            );
          } // foreach
        } // if
      } else {
        $result['leader_id'] = $this->getLeaderId();
        $result['company_id'] = $this->getCompanyId();
      } // if
      
      return $result;
    } // describe
    
    // ---------------------------------------------------
    //  Context
    // ---------------------------------------------------

    /**
     * Return context ID for object that implements this interface
     *
     * @return string
     */
    function getNotifierContextId() {
      return 'PROJECT/' . $this->getId();
    } // getNotifierContextId
    
    /**
     * Return object domain
     * 
     * @return string
     */
    function getObjectContextDomain() {
      return 'projects';
    } // getContextDomain
    
    /**
     * Return object path
     */
    function getObjectContextPath() {
      return 'projects/' . $this->getId();
    } // getContextPath
    
    // ---------------------------------------------------
    //  Project creation
    // ---------------------------------------------------
    
    /**
     * Cached array of reserved slug values
     *
     * @var array
     */
    private $reserved_slugs = false;
    
    /**
     * Returns true if $slug is reserved (by router)
     * 
     * @param string $slug
     * @return boolean
     */
    function isReservedSlug($slug) {
      if($this->reserved_slugs === false) {
        $this->reserved_slugs = array('add', 'archive', 'categories', 'requests');
        
        EventsManager::trigger('on_reserved_project_slugs', array(&$this->reserved_slugs));
      } // if
      
      return in_array($slug, $this->reserved_slugs);
    } // isReservedSlug
    
    /**
     * Cached additional steps value
     *
     * @var NamedList
     */
    protected $additional_steps = false;
    
    /**
     * Return additional steps
     * 
     * @param IUser $user
     * @param Project $template
     * @return NamedList
     */
    function getAdditionalSteps(IUser $user) {
      if($this->additional_steps === false) {
        if($this->getTemplate() instanceof Project) {
          $this->additional_steps = new NamedList(array(
            ProjectCreator::STEP_IMPORT_TEMPLATE => lang('Import template data'), 
            ProjectCreator::STEP_RESCHEDULE => lang('Update milestone and task due dates'), 
          ));
        } else {
          $this->additional_steps = new NamedList(array(
            ProjectCreator::STEP_AUTO_ADD_USERS => lang('Add users who are set to be automatically added to new projects'), 
            ProjectCreator::STEP_MASTER_CATEGORIES => lang('Create default set of categories'), 
          ));
        } // if
        
        if($this->getBasedOn() instanceof ProjectRequest) {
          $this->additional_steps->add(ProjectCreator::STEP_CLOSE_REQUEST, lang('Close project request'));
        } // if
        
        EventsManager::trigger('on_project_additional_steps', array(&$this->additional_steps, &$this, &$user));
      } // if
      
      return $this->additional_steps;
    } // getAdditionalSteps
    
    // ---------------------------------------------------
    //  Default visibility
    // ---------------------------------------------------
    
    /**
     * Return default visibility for all new objects
     * 
     * @return integer
     */
    function getDefaultVisibility() {
      return ConfigOptions::getValueFor('default_project_object_visibility', $this);
    } // getDefaultVisibility
    
    /**
     * Return default visibility for all new objects
     * 
     * @param integer $value
     * @return integer
     */
    function setDefaultVisibility($value) {
      return ConfigOptions::setValueFor('default_project_object_visibility', $this, $value);
    } // setDefaultVisibility
    
    /**
     * Use system default new project object visibility value
     */
    function resetDefaultVisibility() {
      return ConfigOptions::removeValueFor($this, 'default_project_object_visibility');
    } // resetDefaultVisibility
    
    // ---------------------------------------------------
    //  Operations
    // ---------------------------------------------------
    
    /**
     * Return user assignments
     * 
     * @param User $user
     * @return array
     */
    function getUserAssignments(User $user) {
      $filter = new AssignmentFilter();
      
      $filter->filterByUsers(array($user->getId()));
      $filter->filterByProjects(array($this->getId()));
      $filter->setIncludeSubtasks(true);
      $filter->setAdditionalColumn1(AssignmentFilter::ADDITIONAL_COLUMN_CATEGORY);
      $filter->setAdditionalColumn2(AssignmentFilter::ADDITIONAL_COLUMN_MILESTONE);
      $filter->setGroupBy(AssignmentFilter::GROUP_BY_DUE_ON);
      
      try {
        return $filter->run($user);
      } catch(AssignmentFilterConditionsError $e) {
        return null;
      } catch(Exception $e) {
        throw $e;
      } // try
    } // getUserAssignments
    
    /**
     * Copy project items into a destination project
     *
     * @param Project $to
     * @param User $user
     * @throws DBQueryError
     */
    function copyItems(Project &$to) {
      try {
        DB::beginWork('Copying project items @ ' . __CLASS__);
        
        // Clone users
        $this->users()->cloneToProject($to);
        
        // Now move categories
        $categories_map  = array();
        
        $categories = Categories::findBy($this, null);
        
        if($categories) {
          foreach($categories as $category) {
            if($category instanceof ProjectObjectCategory) {
              $copied_category = $category->copyToProject($to, true);
              
              if($copied_category instanceof ProjectObjectCategory) {
                $categories_map[$category->getId()] = $copied_category->getId();
              } // if
            } // if
          } // foreach
        } // if

        // Fix milestone ID-s
        Milestones::fixMilestoneIds($this);
        
        // We need to copy milestones in order to get milestones map
        $milestones = Milestones::findAllByProject($this, VISIBILITY_PRIVATE);
        if($milestones) {
          foreach($milestones as $milestone) {
            $milestone->copyToProject($to, null, $categories_map, true);
          } // foreach
        } // if
        
        $milestoneless_objects = ProjectObjects::findWithoutMilestone($this, STATE_VISIBLE);
        if($milestoneless_objects) {
          $created_on = new DateTimeValue();
          foreach($milestoneless_objects as $object) {
            if($object instanceof ICategory && $object->getCategoryId()) {
              $update_attributes = array(
                'category_id' => isset($categories_map[$object->getCategoryId()]) ? $categories_map[$object->getCategoryId()] : 0,
                'created_on' => $created_on
              );
            } else {
              $update_attributes = null;
            } // if
            
            $object->copyToProject($to, $update_attributes, true);
          } // foreach
        } // if
        
        DB::commit('Project items copied @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to copy project items @ ' . __CLASS__);
        throw $e;
      } // try
    } // copyItems
    
    // ---------------------------------------------------
    //  Permissions
    // ---------------------------------------------------
    
    /**
     * Returns true if $user can view this project
     * 
     * @param User $user
     * @return boolean
     */
    function canView(User $user) {
      return $user->isProjectManager() || $this->users()->isMember($user);
    } // canView
    
    /**
     * Can edit project properties
     *
     * @param User $user
     * @return boolean
     */
    function canEdit(User $user) {
      return $this->isLeader($user) || $user->isProjectManager();
    } // canEdit
    
    /**
     * Returns true if $user can manage budget for this particular project
     * 
     * @param IUser $user
     * @return boolean
     */
    function canManageBudget(IUser $user) {
      if($user instanceof User) {
        return ($this->isLeader($user) || $user->isProjectManager()) && $user->canSeeProjectBudgets();
      } else {
        return false;
      } // if
    } // canManageBudget

    /**
     * Returns true if $user can manage people on this particular projeect
     *
     * @param IUser $user
     * @return boolean
     */
    function canManagePeople(IUser $user) {
      return $this->canEdit($user) || $user->isPeopleManager();
    } // canManagePeople
    
    // ---------------------------------------------------
    //  URL-s
    // ---------------------------------------------------
    
    /**
     * Additional step URL pattern
     *
     * @var string
     */
    private $additional_step_url_pattern = false;
    
    /**
     * Return additional step URL
     * 
     * @param string $step
     * @return string
     */
    function getAdditionalStepUrl($step) {
      if($this->additional_step_url_pattern === false) {
        $this->additional_step_url_pattern = Router::assemble('project_additional_step', array(
          'project_slug' => $this->getSlug(), 
          'step' => '--ADDITIONAL-STEP--', 
        ));
      } // if
      
      return str_replace('--ADDITIONAL-STEP--', $step, $this->additional_step_url_pattern);
    } // getAdditionalStepUrl
    
    /**
     * Return project settings URL
     *
     * @return string
     */
    function getSettingsUrl() {
      return Router::assemble('project_settings', array('project_slug' => $this->getSlug()));
    } // getSettingsUrl
    
    /**
     * Return people URL
     *
     * @return string
     */
    function getPeopleUrl() {
      return Router::assemble('project_people', array('project_slug' => $this->getSlug()));
    } // getPeopleUrl
    
    /**
     * Return add people URL
     *
     * @return string
     */
    function getAddPeopleUrl() {
      return Router::assemble('project_people_add', array('project_slug' => $this->getSlug()));
    } // getAddPeopleUrl
    
    /**
     * Return replace people URL
     *
     * @param User $user
     * @return string
     */
    function getReplaceUserUrl($user) {
      return Router::assemble('project_replace_user', array(
        'project_slug' => $this->getSlug(),
        'user_id' => $user instanceof User ? $user->getId() : $user,
      ));
    } // getReplaceUserUrl
    
    /**
     * Return remove user URL
     *
     * @param User $user
     * @return string
     */
    function getRemoveUserUrl($user) {
      return Router::assemble('project_remove_user', array(
        'project_slug' => $this->getSlug(),
        'user_id' => $user instanceof User ? $user->getId() : $user,
      ));
    } // getRemoveUserUrl
    
    /**
     * Return URL of user permissions page
     *
     * @param User $user
     * @return string
     */
    function getUserPermissionsUrl($user) {
      return Router::assemble('project_user_permissions', array(
        'project_slug' => $this->getSlug(),
        'user_id' => $user instanceof User ? $user->getId() : $user,
      ));
    } // getUserPermissionsUrl

    /**
     * Return project RSS URL
     *
     * @param User $user
     * @return string
     */
    function getRssUrl(User $user) {
      return Router::assemble('project_activity_log_rss', array(
        'project_slug' => $this->getSlug(),
        AngieApplication::API_TOKEN_VARIABLE_NAME => $user->getFeedToken(),
      ));
    } // getRssUrl
    
    // ---------------------------------------------------
    //  Tasks count caching
    // ---------------------------------------------------
    
    /**
     * Return value of total_tasks_count field
     *
     * @return integer
     */
    function getTotalTasksCount() {
      return array_var(ProjectProgress::getProjectProgress($this), 0);
    } // getTotalTasksCount
    
    /**
     * Return value of open_tasks_count field
     *
     * @return integer
     */
    function getOpenTasksCount() {
      return array_var(ProjectProgress::getProjectProgress($this), 1);
    } // getOpenTasksCount
    
    /**
     * Return number of completed tasks in this project
     *
     * @return integer
     */
    function getCompletedTaskCount() {
      return $this->getTotalTasksCount() - $this->getOpenTasksCount();
    } // getCompletedTaskCount
    
    /**
     * Return number of percents this object is done
     *
     * @return integer
     */
    function getPercentsDone() {
      list($total, $open) = ProjectProgress::getProjectProgress($this);
      
      $completed = $total - $open;
      
      if($total && $completed) {
        return floor($completed / $total * 100);
      } // if
      
      return 0;
    } // getPercentsDone
    
    // ---------------------------------------------------
    //  Interface implementations
    // ---------------------------------------------------
    
    /**
     * Return routing context name
     *
     * @return string
     */
    function getRoutingContext() {
      return 'project';
    } // getRoutingContext
    
    /**
     * Return routing context parameters
     *
     * @return mixed
     */
    function getRoutingContextParams() {
      return array(
        'project_slug' => $this->getSlug(),
      );
    } // getRoutingContextParams
    
    /**
     * Cached inspector instance
     * 
     * @var IProjectInspectorImplementation
     */
    private $inspector = false;
    
    /**
     * Return inspector helper instance
     * 
     * @return IProjectInspectorImplementation
     */
    function inspector() {
      if($this->inspector === false) {
        $this->inspector = new IProjectInspectorImplementation($this);
      } // if
      
      return $this->inspector;
    } // inspector
    
    /**
     * Invoice implementation for this object
     * 
     * @var IInvoiceImplementation
     */
    private $invoice;
    
    /**
     * Return invoice implementation
     * 
     * @return IInvoiceBasedOnImplementation
     */
    function invoice() {
      if(empty($this->invoice)) {
        if(AngieApplication::isModuleLoaded('invoicing')) {
          $this->invoice = new IInvoiceBasedOnProjectImplementation($this);
        } else {
          $this->invoice = new IInvoiceBasedOnImplementationStub($this);
        } // if
      }//if
      return $this->invoice;
    }//invoice
    
    /**
     * ProjectAvatar implementation instance for this object
     *
     * @var IProjectAvatarImplementation
     */
  	private $avatar;
    
    /**
     * Return avatar implementation for this object
     *
     * @return IProjectAvatarImplementation
     */
    function avatar() {
      if(empty($this->avatar)) {
        $this->avatar = new IProjectAvatarImplementation($this);
      } // if
      
      return $this->avatar;
    } // avatar
    
    /**
     * Cached search helper instance
     *
     * @var IProjectSearchItemImplementation
     */
    private $search = false;
    
    /**
     * Return search helper instance
     * 
     * @return IProjectSearchItemImplementation
     */
    function search() {
      if($this->search === false) {
        $this->search = new IProjectSearchItemImplementation($this);
      } // if
      
      return $this->search;
    } // search
    
    /**
     * Cached complete implementation instance
     *
     * @var ICompleteImplementation
     */
    private $complete = false;
    
    /**
     * Return complete interface implementation
     *
     * @return ICompleteImplementation
     */
    function complete() {
      if($this->complete === false) {
        $this->complete = new ICompleteImplementation($this);
      } // if
      
      return $this->complete;
    } // complete
    
    /**
     * Users helper instance
     *
     * @var IProjectUsersContextImplementation
     */
    private $users = false;
    
    /**
     * Return users helper implementation
     *
     * @return IProjectUsersContextImplementation
     */
    function users() {
      if($this->users === false) {
        $this->users = new IProjectUsersContextImplementation($this);
      } // if
      
      return $this->users;
    } // users
    
    /**
     * Cached labels implementation instance
     *
     * @var IProjectLabelImplementation
     */
    private $label = false;
    
    /**
     * Return labels implementation instance for this object
     *
     * @return IProjectLabelImplementation
     */
    function label() {
      if($this->label === false) {
        $this->label = new IProjectLabelImplementation($this);
      } // if
      
      return $this->label;
    } // label
    
    /**
     * Categories context
     *
     * @var boolean
     */
    private $categories_context = false;
    
    /**
     * Return categories context implementation
     *
     * @return IProjectCategoriesContextImplementation
     */
    function availableCategories() {
      if($this->categories_context === false) {
        $this->categories_context = new IProjectCategoriesContextImplementation($this);
      } // if
      
      return $this->categories_context;
    } // availableCategories
    
    /**
     * Category helper instance
     *
     * @var IProjectCategoryImplementation
     */
    private $category = false;
    
    /**
     * Return category helper instance
     *
     * @return IProjectCategoryImplementation
     */
    function category() {
      if($this->category === false) {
        $this->category = new IProjectCategoryImplementation($this);
      } // if
      
      return $this->category;
    } // category
    
    /**
     * History helper instance
     *
     * @var IHistoryImplementation
     */
    private $history = false;
    
    /**
     * Return history helper instance
     *
     * @return IHistoryImplementation
     */
    function history() {
      if($this->history === false) {
        $this->history = new IHistoryImplementation($this, array('slug', 'company_id', 'currency_id', 'budget', 'leader_id', 'overview'));
      } // if
      
      return $this->history;
    } // history
    
    /**
     * Cached state helper instance
     *
     * @var IProjectStateImplementation
     */
    private $state = false;
    
    /**
     * Return state helper instance
     *
     * @return IProjectStateImplementation
     */
    function state() {
      if($this->state === false) {
        $this->state = new IProjectStateImplementation($this);
      } // if
      
      return $this->state;
    } // state
    
    /**
     * Tracking helper instance
     *
     * @var ITrackingImplementation
     */
    private $tracking = false;
    
    /**
     * Return tracking helper instance
     *
     * @return ITrackingImplementation
     */
    function tracking() {
      if($this->tracking === false) {
        if(AngieApplication::isModuleLoaded('tracking')) {
          $this->tracking = new ITrackingImplementation($this);
        } else {
          $this->tracking = new ITrackingImplementationStub($this);
        } // if
      } // if
      
      return $this->tracking;
    } // tracking
    
    /**
     * Cached activity logs helper instance
     *
     * @var IActivityLogsImplementation
     */
    private $activity_logs = false;
    
    /**
     * Return activity logs helper
     * 
     * @return IActivityLogsImplementation
     */
    function activityLogs() {
      if($this->activity_logs === false) {
        $this->activity_logs = new IActivityLogsImplementation($this);
      } // if
      
      return $this->activity_logs;
    } // activityLogs
    
    // ---------------------------------------------------
    //  System
    // ---------------------------------------------------
  
    /**
     * Validate model object before save
     *
     * @param ValidationErrors $errors
     */
    function validate(ValidationErrors &$errors) {
      if(!$this->validatePresenceOf('company_id')) {
        $errors->addError(lang('Project client is required'), 'company_id');
      } // if
      
      if(!$this->validatePresenceOf('leader_id') && !$this->validatePresenceOf('leader_name') && !$this->validatePresenceOf('leader_email')) {
        $errors->addError(lang('Project leader is required'), 'leader_id');
      } // if
      
      if(!$this->validatePresenceOf('name', 3)) {
        $errors->addError(lang('Project name is required. Min length is 3 letters'), 'name');
      } // if
      
      if($this->validatePresenceOf('slug')) {
        if(!$this->validateUniquenessOf('slug')) {
          $errors->addError(lang('Short project name needs to unique'), 'slug');
        } // if
      } else {
        $errors->addError(lang('Short project name is required'), 'slug');
      } // if
    } // validate
    
    /**
     * Save project
     *
     * @param Project $template
     * @return boolean
     */
    function save() {
      $modified_fields = $this->getModifiedFields();
      
      try {
        DB::beginWork('Saving project @ ' . __CLASS__);
        
        if($this->isNew()) {
          $slug = trim($this->getSlug());
          
          if($slug) {
            $slug = Inflector::slug($slug);
          } else {
            $slug = Inflector::slug($this->getName());
          } // if
          
          if(is_numeric($slug) || strlen_utf($slug) < 1) {
            $slug = 'unknown-project';
          } elseif(strlen_utf($slug) > 40) {
            $slug = trim(substr_utf($slug, 0, 40), '-');
          } // if
          
          $original_slug = $slug;
          $counter = 1;
          
          while($this->isReservedSlug($slug) || DB::executeFirstCell("SELECT COUNT(id) FROM " . TABLE_PREFIX . "projects WHERE slug = ?", $slug) > 0) {
	        	$slug = $original_slug . '-' . $counter++;
	        } // while
	        
	        $this->setSlug($slug);
        } // if
        
        parent::save();
        
        clean_project_permissions_cache($this);

        if (in_array("completed_on", $modified_fields)) {
          clean_quick_jump_and_quick_add_cache();
        } // if
        
        DB::commit('Project saved @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to save project @ ' . __CLASS__);
        
        throw $e;
      } // try
      
      return true; // Legacy
    } // save
    
    /**
     * Delete project and all realted data
     *
     * @return boolean
     */
    function delete() {
      try {
        DB::beginWork('Deleting project @ ' . __CLASS__);
        
        parent::delete();
        
        $this->users()->clear(); // clear project user records
        
        ProjectObjects::deleteByProject($this);
        Favorites::deleteByParent($this);
        
        clean_project_permissions_cache($this);
        EventsManager::trigger('on_project_deleted', array($this));
        
        DB::commit('Project deleted @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to delete project @ ' . __CLASS__);
        throw $e;
      } // try
      
      return true;
    } // delete
    
    /**
     * Cached tab settings
     *
     * @var array
     */
    private $project_tabs = array();
    
    /**
     * Return available tabs
     *
     * @param User user
     * @param string $interface
     * @return NamedList
     */
    function getTabs(User $user, $interface = AngieApplication::INTERFACE_DEFAULT, $use_cache = true) {
      $user_id = $user->getId();
      
      if((empty($this->project_tabs[$user_id]) || empty($this->project_tabs[$user_id][$interface])) || !$use_cache) {
        $tabs_settings = (array) ConfigOptions::getValueFor('project_tabs', $this);
      
        $tabs = new NamedList();
        
        // Register milestones tab
        if(in_array('milestones', $tabs_settings) && Milestones::canAccess($user, $this, false)) {
        	$tabs->add('milestones', array(
            'text' => lang('Milestones'),
            'url' => Router::assemble('project_milestones', array('project_slug' => $this->getSlug())),
            'icon' => $interface == AngieApplication::INTERFACE_DEFAULT ? 
            	AngieApplication::getImageUrl('icons/16x16/milestones-tab-icon.png', SYSTEM_MODULE) : 
            	AngieApplication::getImageUrl('icons/listviews/milestones.png', SYSTEM_MODULE, AngieApplication::INTERFACE_PHONE)
          ));

          // Register outline tab
          if ($interface == AngieApplication::INTERFACE_DEFAULT && in_array('outline', $tabs_settings)) {
            $tabs->add('outline', array(
              'text' => lang('Outline'),
              'url' => Router::assemble('project_outline', array(
                'project_slug' => $this->getSlug()
              )),
              'icon' => AngieApplication::getImageUrl('icons/16x16/outline-tab-icon.png', SYSTEM_MODULE)
            ));
          } // if
        } // if
        
        EventsManager::trigger('on_project_tabs', array(&$tabs, &$user, &$this, &$tabs_settings, $interface));
        
        $separator_counter = 1;
        
        $result = new NamedList();
        
        if($interface == AngieApplication::INTERFACE_DEFAULT) {
          $result->add('overview', array(
            'text' => str_excerpt($this->getName(), 25),
            'url' => $this->getViewUrl(),
          	'icon' => $this->avatar()->getUrl(IProjectAvatarImplementation::SIZE_SMALL)
          ));
          
          $result->add('separator-' . $separator_counter++, array(
            'text' => '-',
            'url' => null,
          ));
        } // if
        
        foreach($tabs_settings as $add_tab) {
          if($add_tab == '-') {
            $result->add('separator-' . $separator_counter++, array(
              'text' => '-',
              'url' => null,
            ));
          } else {
            if(isset($tabs[$add_tab])) {
              $result->add($add_tab, $tabs[$add_tab]);
            } // if
          } // if
        } // foreach
        
        if(!array_key_exists($user_id, $this->project_tabs)) {
          $this->project_tabs[$user->getId()] = array();
        } // if
          
        $this->project_tabs[$user->getId()][$interface] = $result;
      } // if
      
      return $this->project_tabs[$user->getId()][$interface];
    } // getTabs
    
    /**
     * Returns true if this project has $tab_name tab
     * 
     * @param string $tab_name
     * @param User $user
     * @param string $interface
     * @return boolean
     */
    function hasTab($tab_name, User $user, $interface = AngieApplication::INTERFACE_DEFAULT) {
      $tabs = $this->getTabs($user, $interface);
      
      return $tabs instanceof NamedList && $tabs->exists($tab_name);
    } // hasTab
  
  }