<?php

  /**
   * Framework level attachments manager implementation
   *
   * @package angie.frameworks.attachments
   * @subpackage models
   */
  abstract class FwAttachments extends BaseAttachments {
    
    /**
     * Return attachments by parent object
     *
     * @param IAttachments $object
     * @return DBResult
     */
    static function findByParent(IAttachments $parent) {
      return self::find(array(
        'conditions' => array('parent_type = ? AND parent_id = ? AND state >= ?', get_class($parent), $parent->getId(), ($parent instanceof IState ? $parent->getState() : STATE_VISIBLE)),
        'order' => 'created_on',
      ));
    } // findByParent
    
    /**
     * Return number of attachments for a given object
     *
     * @param IAttachments $parent
     * @param IUser $user
     * @param boolean $use_cache
     * @return integer
     */
    static function countByParent(IAttachments $parent, $user = null, $use_cache = true) {
      $parent_type= get_class($parent);
      $parent_id = $parent->getId();
      
      if($use_cache) {
        $cached_value = cache_get('attachments_count');
        if(!is_array($cached_value)) {
          $cached_value = array();
          
          $rows = DB::execute('SELECT parent_type, parent_id, COUNT(id) AS attachments_count FROM ' . TABLE_PREFIX . 'attachments WHERE parent_type != ? AND parent_type IS NOT NULL AND state >= ? GROUP BY parent_type, parent_id', '', STATE_ARCHIVED);
          if($rows) {
            foreach($rows as $row) {
              if(!isset($cached_value[$row['parent_type']])) {
                $cached_value[$row['parent_type']] = array();
              } // if
              
              $cached_value[$row['parent_type']][(integer) $row['parent_id']] = (integer) $row['attachments_count'];
            } // foreach
          } // if
          
          cache_set('attachments_count', $cached_value);
        } // if
        
        return isset($cached_value[$parent_type]) && isset($cached_value[$parent_type][$parent_id]) ? $cached_value[$parent_type][$parent_id] : 0;
      } // if
      
      return self::count(array('parent_type = ? AND parent_id = ? AND state >= ?', $parent_type, $parent_id, ($parent instanceof IState ? $parent->getState() : STATE_VISIBLE)));
    } // countByParent
    
    /**
     * Delete records from attachments table that match given $conditions
     *
     * This function also deletes all files from /upload folder so this function
     * is not 100% transaction safe
     *
     * @param mixed $conditions
     * @return boolean
     */
    static function delete($conditions = null) {
      $attachments_table = TABLE_PREFIX . 'attachments';
      $object_contexts_table = TABLE_PREFIX . 'object_contexts';
      
      try {
        DB::beginWork('Deleting attachments @ ' . __CLASS__);
        
        $perpared_conditions = DB::prepareConditions($conditions);
        $where_string = trim($perpared_conditions) == '' ? '' : "WHERE $perpared_conditions";
  
        $rows = DB::execute("SELECT id, location FROM $attachments_table $where_string");
        if(is_foreachable($rows)) {

          // create id => location map
          $attachments = array();
          foreach($rows as $row) {
            $attachments[(integer) $row['id']] = $row['location'];
          } // foreach

          // get attachment ids
          $attachment_ids = array_keys($attachments);

          // delete object contexts for these attachments
          DB::execute("DELETE FROM $object_contexts_table WHERE parent_type = ? AND parent_id IN (?)", 'Attachment', $attachment_ids);

          // delete attachments themselves
          DB::execute("DELETE FROM $attachments_table WHERE id IN (?)", $attachment_ids);

          // delete attachments from disk
          foreach($attachments as $location) {
            @unlink(UPLOAD_PATH . '/' . $location);
          } // foreach
        } // if
        
        DB::commit('Attachments deleted @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to delete attachments @ ' . __CLASS__);
        
        throw $e;
      } // try
      
      return true;
    } // delete
    
    /**
     * Clone attachments
     *
     * To remove all attachments attached to the $destination object
     *
     * @param IAttachments $original
     * @param IAttachments $destination
     * @return boolean
     */
    static function cloneAttachments(IAttachments $original, IAttachments $destination) {
      try {
        DB::beginWork('Cloning attachments @ ' . __CLASS__);
        
        $new_files = array();
        
        $attachments = self::findByObject($original);
        if(is_foreachable($attachments)) {
          $to_insert = array();
          
          foreach($attachments as $attachment) {
            $source_file = $attachment->getFilePath();
            $target_file = AngieApplication::getAvailableUploadsFileName();
            
            if(copy($source_file, $target_file)) {
              $new_files[] = $target_file;
              $to_insert[] = DB::prepare("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", array(get_class($destination), $destination->getId(), $attachment->getName(), $attachment->getMimeType(), $attachment->getSize(), basename($target_file), $attachment->getAttachmentType(), $attachment->getCreatedOn(), $attachment->getCreatedById(), $attachment->getCreatedByName(), $attachment->getCreatedByEmail()));
            } // if
          } // foreach
          
          if(is_foreachable($to_insert)) {
            DB::execute("INSERT INTO " . TABLE_PREFIX . 'attachments (parent_type, parent_id, name, mime_type, size, location, attachment_type, created_on, created_by_id, created_by_name, created_by_email) VALUES ' . implode(', ', $to_insert));
          } // if
        } // if
        
        DB::commit('Attachments cloned @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to clone attachments @ ' . __CLASS__);
        
        if(is_foreachable($new_files)) {
          foreach($new_files as $new_file) {
            @unlink($new_file);
          } // if
        } // if
        
        throw $e;
      } // try
      
      return true;
    } // cloneAttachments
    
    /**
     * Clean up oprhaned attachments
     *
     * This function deletes all attachments that are older than 2 days and have
     * no parent set
     *
     * @return boolean
     */
    static function cleanUp() {
      $attachment_ids = DB::executeFirstColumn('SELECT id FROM ' . TABLE_PREFIX . 'attachments WHERE (parent_id IS NULL OR parent_id = 0) AND created_on < ?', new DateTimeValue('-2 days'));

      if ($attachment_ids) {
        try {
          DB::beginWork('Attachment cleanup started');
          $result = self::delete(array('id IN (?)', $attachment_ids)); // Delete the attachments
          DB::commit('Attachment cleanup finished');
          return $result;
        } catch (Exception $e) {
          DB::rollback('Attachment cleanup failed');
          throw $e;
        } // try
      } // if
    } // cleanUp
    
    // ---------------------------------------------------
    //  State
    // ---------------------------------------------------
    
    /**
     * Archive subtasks attached to a given parent object
     *
     * @param IAttachments $parent
     */
    static function archiveByParent(IAttachments $parent) {
      if($parent instanceof IState) {
        $parent->state()->archiveSubitems(TABLE_PREFIX . 'attachments', array('parent_type = ? AND parent_id = ?', get_class($parent), $parent->getId()));
      } else {
        throw new InvalidInstanceError('parent', $parent, 'IState');
      } // if
    } // archiveByParent
    
    /**
     * Unarchive subtasks attached to a given parent object
     *
     * @param IAttachments $parent
     */
    static function unarchiveByParent(IAttachments $parent) {
      if($parent instanceof IState) {
        $parent->state()->unarchiveSubitems(TABLE_PREFIX . 'attachments', array('parent_type = ? AND parent_id = ?', get_class($parent), $parent->getId()));
      } else {
        throw new InvalidInstanceError('parent', $parent, 'IState');
      } // if
    } // unarchiveByParent
    
    /**
     * Trash subtasks attached to a given parent object
     *
     * @param IAttachments $parent
     */
    static function trashByParent(IAttachments $parent) {
      if($parent instanceof IState) {
        $parent->state()->trashSubitems(TABLE_PREFIX . 'attachments', array('parent_type = ? AND parent_id = ?', get_class($parent), $parent->getId()));
      } else {
        throw new InvalidInstanceError('parent', $parent, 'IState');
      } // if
    } // trashByParent
    
    /**
     * Restore from trash subtasks attached to a given parent object
     *
     * @param IAttachments $parent
     */
    static function untrashByParent(IAttachments $parent) {
      if($parent instanceof IState) {
        $parent->state()->untrashSubitems(TABLE_PREFIX . 'attachments', array('parent_type = ? AND parent_id = ?', get_class($parent), $parent->getId()));
      } else {
        throw new InvalidInstanceError('parent', $parent, 'IState');
      } // if
    } // untrashByParent
    
    /**
     * Trash subtasks attached to a given parent object
     *
     * @param IAttachments $parent
     * @param boolean $soft
     */
    static function deleteByParent(IAttachments $parent, $soft = true) {
      if($soft && $parent instanceof IState) {
        $parent->state()->deleteSubitems(TABLE_PREFIX . 'attachments', array('parent_type = ? AND parent_id = ?', get_class($parent), $parent->getId()));
      } else {
        self::delete(array("parent_type = ? AND parent_id = ?", get_class($parent), $parent->getId()));
      } // if
    } // deleteByParent
    
    /**
     * Delete entries by parents
     * 
     * $parents is an array where key is parent type and value is array of 
     * object ID-s of that particular parent
     * 
     * @param array $parents
     */
    static function deleteByParents($parents) {
      if (is_foreachable($parents)) {
        $conditions = array();
        foreach($parents as $parent_type => $parent_ids) {
          $conditions[] = DB::prepareConditions(array('(parent_type = ? AND parent_id IN (?))', $parent_type, $parent_ids));
        } // foreach
        $conditions = implode(' OR ', $conditions);

        self::delete(array($conditions));
      } // if
    } // deleteByParents
    
    /**
     * Remove attachments by parent types
     * 
     * @param array $types
     */
    static function deleteByParentTypes($types) {
      self::delete(array('parent_type IN (?)', $types));
    } // deleteByParentTypes
    
    // ---------------------------------------------------
    //  Cache
    // ---------------------------------------------------
    
    /**
     * Name of the cache variable where we'll cache counts
     *
     * @var string
     */
    const COUNT_CACHE_INDEX = 'attachments_count';
    
    /**
     * Refresh attachments count cache
     * 
     * @param mixed $context
     * @return array
     */
    static function refreshCountCache($context = null) {
      if($context instanceof IAttachments) {
        $cached_value = cache_get(Attachments::COUNT_CACHE_INDEX);
        
        if(is_array($cached_value)) {
          $parent_type = get_class($context);
          $parent_id = $context->getId();
          
          if(!isset($cached_value[$parent_type])) {
            $cached_value[$parent_type] = array();
          } // if
          
          $cached_value[$parent_type][$parent_id] = (integer) DB::executeFirstCell('SELECT COUNT(id) FROM ' . TABLE_PREFIX . 'attachments WHERE parent_type = ? AND parent_id = ? AND state >= ? GROUP BY parent_type, parent_id', $parent_type, $parent_id, STATE_ARCHIVED);
          
          cache_set(Attachments::COUNT_CACHE_INDEX, $cached_value);
        } // if
        
        return $cached_value;
        
      // Rebuild entire counts cache
      } elseif($context === null) {
        $cached_value = array();
          
        $rows = DB::execute('SELECT parent_type, parent_id, COUNT(id) AS attachments_count FROM ' . TABLE_PREFIX . 'attachments WHERE state >= ? GROUP BY parent_type, parent_id', STATE_ARCHIVED);
        if($rows) {
          foreach($rows as $row) {
            if(!isset($cached_value[$row['parent_type']])) {
              $cached_value[$row['parent_type']] = array();
            } // if
            
            $cached_value[$row['parent_type']][(integer) $row['parent_id']] = (integer) $row['attachments_count'];
          } // foreach
        } // if
        
        cache_set('attachments_count', $cached_value);
        
        return $cached_value;
      } else {
        throw new InvalidInstanceError('context', $context, 'IAttachments', '$context should be NULL or valid IAttachments instance');
      } // if
    } // refreshCountCache
    
    /**
     * Get trashed map
     * 
     * @param User $user
     * @return array
     */
    static function getTrashedMap($user) {
    	$trashed_attachments = DB::execute('SELECT id, type FROM ' . TABLE_PREFIX . 'attachments WHERE state = ?', STATE_TRASHED);
    	    	
    	if (!is_foreachable($trashed_attachments)) {
    		return null;
    	} // if
    	
    	$result = array();
    	
    	foreach ($trashed_attachments as $trashed_attachment) {
    		$type = strtolower($trashed_attachment['type']);
    		
    		if (!isset($result[$type])) {
    			$result[$type] = array();
    		} // if 
    		
    		$result[$type][] = $trashed_attachment['id'];
    	} // foreach
    	
    	return $result;
    } // getTrashedMap
    
    /**
     * Find trashed attachments
     * 
     * @param User $user
     * @param array $map
     * @return array
     */
    static function findTrashed(User $user, &$map) {
    	$query = Trash::getParentQuery($map);    	
    	if ($query) {
	    	$trashed_attachments = DB::execute('SELECT id, name, type, parent_id, parent_type FROM ' . TABLE_PREFIX . 'attachments WHERE state = ? AND ' . $query . ' ORDER BY created_on DESC', STATE_TRASHED);
    	} else {
    		$trashed_attachments = DB::execute('SELECT id, name, type, parent_id, parent_type FROM ' . TABLE_PREFIX . 'attachments WHERE state = ? ORDER BY created_on DESC', STATE_TRASHED);
    	} // if
    	
    	if (!is_foreachable($trashed_attachments)) {
    		return null;
    	} // if
    	
    	$items = array();
    	foreach ($trashed_attachments as $attachment) {
    		$items[] = array(
    			'id'						=> $attachment['id'],
    			'name'					=> $attachment['name'],
    			'type'					=> $attachment['type'],
    		);   		
    	} // foreach
    	
    	return $items;
    } // findTrashed
    
    /**
     * Delete trashed attachments
     * 
     * @param User $user
     * @return boolean
     */
    static function deleteTrashed(User $user) {
    	$attachments = Attachments::find(array(
    		'conditions' => array('state = ?', STATE_TRASHED)
    	));
    	
    	if (is_foreachable($attachments)) {
    		foreach ($attachments as $attachment) {
    			$attachment->state()->delete();
    		} // foreach
    	} // if
    	
    	return true;
    } // deleteTrashed


    /**
     * Does attachment / file has preview
     *
     * @param mixed $item
     * @return boolean
     */
    static function hasPreview($item) {
      return (boolean) self::getPreviewType($item);
    } // hasPreview

    /**
     * Get Preview type
     *
     * @param mixed $item
     * @return string
     */
    static function getPreviewType($item) {
      list($name, $path, $mime) = Attachments::getFileMeta($item);

      // try to determine preview type by mime type
      if (in_array($mime, array('image/jpg', 'image/jpeg', 'image/pjpeg', 'image/gif', 'image/png'))) {
        return DOWNLOAD_PREVIEW_IMAGE;
      } // if

      $file_extension = strtolower(get_file_extension($name));

      // determine preview type by extension
      switch ($file_extension) {
        case 'flv': return DOWNLOAD_PREVIEW_VIDEO; break;
        case 'mp4': return DOWNLOAD_PREVIEW_VIDEO; break;
        case 'mp3': return DOWNLOAD_PREVIEW_AUDIO; break;
        case 'aac': return DOWNLOAD_PREVIEW_AUDIO; break;
        case 'swf': return DOWNLOAD_PREVIEW_FLASH; break;
      } // switch

      return false;
    } // getPreviewType

    /**
     * Get File meta
     *
     * @param mixed $item
     * @return array
     */
    static function getFileMeta($item) {
      if (is_int($item) || ($item instanceof Attachment) || ($item instanceof FileVersion) || ($item instanceof File)) {
        $attachment = is_int($item) ? Attachments::findById($item) : $item;
        $name = $attachment->getName();
        $path = $attachment->getLocation();
        $mime = $attachment->getMimeType();
      } else if (is_array($item)) {
        $name = $item[0];
        $path = $item[1];
        $mime = get_mime_type($path, $name);
      } else if (is_file($item)) {
        $path_info = pathinfo($item);
        $name = $path_info['basename'];
        $path = $item;
        $mime = get_mime_type($path, $name);
      } else {
        $name = null;
        $path = null;
        $mime = null;
      } // if

      return array($name, $path, $mime);
    } // getFileMeta

  }