<?php

  /**
   * Project scheduler implementation
   *
   * @package activeCollab.modules.system
   * @subpackage models
   */

  /**
   * All project schedule related operations
   */
  final class ProjectScheduler {
    
    /**
     * Change project start date
     * 
     * This method changes project start date while making sure that start and 
     * due dates of project items is up to date with the change
     *
     * @param Project $project
     * @param DateValue $new_start_date
     * @param boolean $skip_days_off
     */
    static function rescheduleProject(Project $project, DateValue $reference_first_milestone_date, DateValue $new_first_milestone_date, $skip_days_off = false) {
      try {
        DB::beginWork('Rescheduling project @ ' . __CLASS__);
        
        $project_objects_table = TABLE_PREFIX . 'project_objects';
      
        $diff = $new_first_milestone_date->getTimestamp() - $reference_first_milestone_date->getTimestamp();
        
        // Get milestones order by start date, that are not TBD
        $milestones = Milestones::find(array(
          'conditions' => array('project_id = ? AND date_field_1 IS NOT NULL AND due_on IS NOT NULL', $project->getId()),
          'order' => 'date_field_1'
        ));
        
        // Reschedule milestones and cascade rescheduling to related objects
        self::rescheduleMilestones($milestones, $diff, true, $skip_days_off);
        
        // Reschedule objects that don't have milestone set
        $reschedule_ids = DB::executeFirstColumn("SELECT id FROM $project_objects_table WHERE type IN (?) AND project_id = ? AND state >= ? AND (milestone_id IS NULL OR milestone_id = ?)", array('Task', 'TodoList'), $project->getId(), STATE_ARCHIVED, 0);
        
        if($reschedule_ids) {
          self::advanceProjectItems($reschedule_ids, $diff, $skip_days_off);
        } // if
        
        DB::commit('Project rescheduled @ ' . __CLASS__);
        
        cache_remove('acx_project_objects_*');
      } catch(Exception $e) {
        DB::rollback('Failed to reschedule project @ ' . __CLASS__);
        throw $e;
      } // try
    } // rescheduleProject
    
    /**
     * Reschedule more than one milestone
     *
     * @param array $milestones
     * @param integer $advance
     * @param boolean $reschedule_tasks
     * @param boolean $skip_days_off
     */
    static function rescheduleMilestones($milestones, $advance, $reschedule_tasks = false, $skip_days_off = false) {
      try {
        DB::beginWork('Rescheduling milestones @ ' . __CLASS__);
        
        $advance_seconds = $advance; // Start value
        
        foreach($milestones as $milestone) {
          $start_on = $milestone->getStartOn();
          $due_on = $milestone->getDueOn();
          
          if($start_on instanceof DateValue && $due_on instanceof DateValue) {
            list($start_on_moved, $due_on_moved) = self::rescheduleMilestone($milestone, new DateValue($start_on->getTimestamp() + $advance_seconds), new DateValue($due_on->getTimestamp() + $advance_seconds), $reschedule_tasks, $skip_days_off);            
            if($start_on_moved) {
              $advance_seconds += $start_on_moved * 86400;
            } // if
          } // if
        } // foreach
        
        DB::commit('Rescheduled milestones @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to reschedule milestones @ ' . __CLASS__);
        
        throw $e;
      } // try
    } // rescheduleMilestones
    
    /**
     * Reschedule milestone
     * 
     * Change start and / or due date of a milestone while taking care of start 
     * and due dates of the other milestones and tasks in the project
     *
     * @param Milestone $milestone
     * @param DateValue $new_start_date
     * @param DateValue $new_due_date
     * @param boolean $reschedule_tasks
     * @param boolean $skip_days_off
     */
    static function rescheduleMilestone(Milestone $milestone, DateValue $new_start_date, DateValue $new_due_date, $reschedule_tasks = false, $skip_days_off = false) {
      $project_objects_table = TABLE_PREFIX . 'project_objects';
      
      $start_on = $milestone->getStartOn();
      $due_on = $milestone->getDueOn();
      
      $start_on_moved = 0;
      $due_on_moved = 0;
      
      if($skip_days_off) {
        while(!Globalization::isWorkday($new_start_date)) {
          $new_start_date->advance(86400);
          $start_on_moved++;
        } // if
        
        if($start_on_moved) {
          $new_due_date->advance(86400 * $start_on_moved);
        } // if
        
        while(!Globalization::isWorkday($new_due_date)) {
          $new_due_date->advance(86400);
          $due_on_moved++;
        } // if
      } // if
      
      try {
        DB::beginWork('Rescheduling milestone @ ' . __CLASS__);
        
        $milestone->setStartOn($new_start_date);
        $milestone->setDueOn($new_due_date);
        
        $milestone->save();
        
        // Reschedule all related tasks...
        if($reschedule_tasks && $start_on instanceof DateValue && $due_on instanceof DateValue) {
          $milestone_object_ids = DB::executeFirstColumn("SELECT id FROM $project_objects_table WHERE type IN (?) AND milestone_id = ? AND state >= ?", array('TodoList', 'Task'), $milestone->getId(), STATE_ARCHIVED);
          
          if($milestone_object_ids) {
            self::advanceProjectItems($milestone_object_ids, $new_start_date->getTimestamp() - $start_on->getTimestamp(), $skip_days_off);
          } // if
        } // if
        
        DB::commit('Milestone rescheduled @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to reschedule milestone @ ' . __CLASS__);
        
        throw $e;
      } // try
      
      return array($start_on_moved, $due_on_moved);
    } // rescheduleMilestone
    
    /**
     * Reschedule a single project object
     * 
     * @param ProjectObject $object
     * @param DateValue $due_on
     * @param boolean $reschedule_subtasks
     * @param boolean $skip_days_off
     */
    static function rescheduleProjectObject(ProjectObject $object, DateValue $due_on, $reschedule_subtasks = true, $skip_days_off = true) {
      if($object instanceof ProjectObject && $object->fieldExists('due_on') && $due_on instanceof DateValue) {
        try {
          if($skip_days_off) {
            while(!Globalization::isWorkday($due_on)) {
              $due_on->advance(86400);
            } // while
          } // if
          
          DB::beginWork('Rescheduling project object @ ' . __CLASS__);
          
          $old_due_on = $object->getDueOn();
          
          $object->setDueOn($due_on);
          $object->save();
          
          if($reschedule_subtasks && $old_due_on instanceof DateValue && $object instanceof ISubtasks) {
            $object->subtasks()->advanceDueDates($due_on->getTimestamp() - $old_due_on->getTimestamp(), $skip_days_off);
          } // if
          
          DB::commit('Project object rescheduled @ ' . __CLASS__);
        } catch(Exception $e) {
          DB::rollback('Failed to reschedule project object @ ' . __CLASS__);
          throw $e;
        } // try
      } // if
    } // rescheduleProjectObject
    
    /**
     * Advance give objects by number of seconds
     *
     * @param array $item_ids
     * @param integer $advance
     * @param boolean $skip_days_off
     */
    private static function advanceProjectItems($item_ids, $advance, $skip_days_off) {
      if($advance && is_foreachable($item_ids)) {
        try {
          $project_objects_table = TABLE_PREFIX . 'project_objects';
          
          DB::beginWork('Advance project items @ ' . __CLASS__);
          
          if($skip_days_off) {
            $items = DB::execute("SELECT id, type, due_on FROM $project_objects_table WHERE id IN (?) AND due_on IS NOT NULL AND completed_on IS NULL", $item_ids);
            
            if($items) {
              foreach($items as $item) {
                $item_due_on = new DateValue($item['due_on']);
                
                $original_timestamp = $item_due_on->getTimestamp(); // Remember old timestamp, we'll need it later
                
                if($advance) {
                  $item_due_on->advance($advance);
                } // if
                
                while(!Globalization::isWorkday($item_due_on)) {
                  $item_due_on->advance(86400);
                } // while
                
                if($item_due_on_diff = $item_due_on->getTimestamp() - $original_timestamp) {
                  DB::execute("UPDATE $project_objects_table SET due_on = ? WHERE id = ?", $item_due_on, $item['id']);
                  Subtasks::advanceByParent(array($item['type'], $item['id']), $item_due_on_diff, true);
                } // if
              } // foreach
            } // if
          } // if
          
          cache_remove('acx_project_objects_*');
          
          DB::commit('Project items advanced @ ' . __CLASS__);
        } catch(Exception $e) {
          DB::rollback('Failed to advance project items @ ' . __CLASS__);
          
          throw $e;
        } // try
      } // if
    } // advanceProjectItems

  }