<?php

/**
 *
 * @package Airtime
 * @copyright 2010 Sourcefabric O.P.S.
 * @license http://www.gnu.org/licenses/gpl.txt
 */
class Application_Model_Playlist implements Application_Model_LibraryEditable
{
	const CUE_ALL_ERROR = 0;
	const CUE_IN_ERROR = 1;
	const CUE_OUT_ERROR = 2;
	
    /**
     * propel connection object.
     */
    private $con;

    /**
     * unique id for the playlist.
     */
    private $id;

    /**
     * propel object for this playlist.
     */
    private $pl;

    /**
     * info needed to insert a new playlist element.
     */
    private $plItem = array(
        "id" => "",
        "pos" => "",
        "cliplength" => "",
        "cuein" => "00:00:00",
        "cueout" => "00:00:00",
        "fadein" => "0.0",
        "fadeout" => "0.0",
    	"crossfadeDuration" => 0
    );

    //using propel's phpNames.
    private $categories = array(
        "dc:title" => "Name",
        "dc:creator" => "Creator",
        "dc:description" => "Description",
        "dcterms:extent" => "Length"
    );

    public function __construct($id=null, $con=null)
    {
        if (isset($id)) {
            $this->pl = CcPlaylistQuery::create()->findPK($id);

            if (is_null($this->pl)) {
                throw new PlaylistNotFoundException();
            }
        } else {
            $this->pl = new CcPlaylist();
            $this->pl->setDbUTime(new DateTime("now", new DateTimeZone("UTC")));
            $this->pl->save();
        }

        $this->plItem["fadein"] = Application_Model_Preference::GetDefaultFadeIn();
        $this->plItem["fadeout"] = Application_Model_Preference::GetDefaultFadeOut();
        $this->plItem["crossfadeDuration"] = Application_Model_Preference::GetDefaultCrossfadeDuration();

        $this->con = isset($con) ? $con : Propel::getConnection(CcPlaylistPeer::DATABASE_NAME);
        $this->id = $this->pl->getDbId();
    }

    /**
     * Return local ID of virtual file.
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

     /**
     * Rename stored virtual playlist
     *
     * @param string $p_newname
     */
    public function setName($p_newname)
    {
        $this->pl->setDbName($p_newname);
        $this->pl->setDbMtime(new DateTime("now", new DateTimeZone("UTC")));
        $this->pl->save($this->con);
    }

     /**
     * Get mnemonic playlist name
     *
     * @return string
     */
    public function getName()
    {
        return $this->pl->getDbName();
    }

    public function setDescription($p_description)
    {
        $this->pl->setDbDescription($p_description);
        $this->pl->setDbMtime(new DateTime("now", new DateTimeZone("UTC")));
        $this->pl->save($this->con);
    }

    public function getDescription()
    {
        return $this->pl->getDbDescription();
    }

    public function getCreator()
    {
        return $this->pl->getCcSubjs()->getDbLogin();
    }

    public function getCreatorId()
    {
        return $this->pl->getCcSubjs()->getDbId();
    }

    public function setCreator($p_id)
    {
        $this->pl->setDbCreatorId($p_id);
        $this->pl->setDbMtime(new DateTime("now", new DateTimeZone("UTC")));
        $this->pl->save($this->con);
    }

    public function getLastModified($format = null)
    {
        //Logging::info($this->pl->getDbMtime($format));
        //Logging::info($this->pl);
        return $this->pl->getDbMtime($format);
    }

    public function getSize()
    {
        return $this->pl->countCcPlaylistcontentss();
    }

    /**
     * Get the entire playlist as a two dimentional array, sorted in order of play.
     * @param boolean $filterFiles if this is true, it will only return files that has
     *             file_exists flag set to true
     * @return array
     */
    public function getContents($filterFiles=false)
    {
        $sql = <<<SQL
  SELECT *
   FROM (
           (SELECT pc.id AS id,
                   pc.type,
                   pc.position,
                   pc.cliplength AS LENGTH,
                   pc.cuein,
                   pc.cueout,
                   pc.fadein,
                   pc.fadeout,
                   pc.trackoffset,
                   f.id AS item_id,
                   f.track_title,
                   f.artist_name AS creator,
                   f.file_exists AS EXISTS,
                   f.filepath AS path,
                   f.length AS orig_length,
                   f.mime AS mime
            FROM cc_playlistcontents AS pc
            JOIN cc_files AS f ON pc.file_id=f.id
            WHERE pc.playlist_id = :playlist_id1
SQL;

        if ($filterFiles) {
            $sql .= <<<SQL
            AND f.file_exists = :file_exists
SQL;
        }
        $sql .= <<<SQL
              AND TYPE = 0)
         UNION ALL
           (SELECT pc.id AS id,
                   pc.TYPE, pc.position,
                            pc.cliplength AS LENGTH,
                            pc.cuein,
                            pc.cueout,
                            pc.fadein,
                            pc.fadeout,
                            pc.trackoffset,
                            ws.id AS item_id,
                            (ws.name || ': ' || ws.url) AS title,
                            sub.login AS creator,
                            't'::boolean AS EXISTS,
                            ws.url AS path,
                            ws.length AS orig_length,
                            ws.mime as mime
            FROM cc_playlistcontents AS pc
            JOIN cc_webstream AS ws ON pc.stream_id=ws.id
            LEFT JOIN cc_subjs AS sub ON sub.id = ws.creator_id
            WHERE pc.playlist_id = :playlist_id2
              AND pc.TYPE = 1)
         UNION ALL
           (SELECT pc.id AS id,
                   pc.TYPE, pc.position,
                            pc.cliplength AS LENGTH,
                            pc.cuein,
                            pc.cueout,
                            pc.fadein,
                            pc.fadeout,
                            pc.trackoffset,
                            bl.id AS item_id,
                            bl.name AS title,
                            sbj.login AS creator,
                            't'::boolean AS EXISTS,
                            NULL::text AS path,
                            bl.length AS orig_length,
                            NULL::text as mime
            FROM cc_playlistcontents AS pc
            JOIN cc_block AS bl ON pc.block_id=bl.id
            JOIN cc_subjs AS sbj ON bl.creator_id=sbj.id
            WHERE pc.playlist_id = :playlist_id3
              AND pc.TYPE = 2)) AS temp
   ORDER BY temp.position;
SQL;
        //Logging::info($sql);

        $params = array(
            ':playlist_id1'=>$this->id, ':playlist_id2'=>$this->id, ':playlist_id3'=>$this->id);
        if ($filterFiles) {
            $params[':file_exists'] = $filterFiles;
        }

        $rows = Application_Common_Database::prepareAndExecute($sql, $params);

        $offset = 0;
        foreach ($rows as &$row) {
        	
        	//Logging::info($row);
        	
            $clipSec = Application_Common_DateHelper::playlistTimeToSeconds($row['length']);
            $row['trackSec'] = $clipSec;
            
            $row['cueInSec'] = Application_Common_DateHelper::playlistTimeToSeconds($row['cuein']);
            $row['cueOutSec'] = Application_Common_DateHelper::playlistTimeToSeconds($row['cueout']);
            
            $trackoffset = $row['trackoffset'];
            $offset += $clipSec;
            $offset -= $trackoffset;
            $offset_cliplength = Application_Common_DateHelper::secondsToPlaylistTime($offset);

            //format the length for UI.
            if ($row['type'] == 2) {
                $bl = new Application_Model_Block($row['item_id']);
                $formatter = new LengthFormatter($bl->getLength());
            } else {
                $formatter = new LengthFormatter($row['length']);
            }
            $row['length'] = $formatter->format();

            $formatter = new LengthFormatter($offset_cliplength);
            $row['offset'] = $formatter->format();

            //format the fades in format 00(.000000)
            $fades = $this->getFadeInfo($row['position']);
            $row['fadein'] = $fades[0];
            $row['fadeout'] = $fades[1];
            
            // format the cues in format 00:00:00(.0)
            // we need to add the '.0' for cues and not fades
            // because propel takes care of this for us
            // (we use propel to fetch the fades)
            $row['cuein'] = str_pad(substr($row['cuein'], 0, 10), 10, '.0');
            $row['cueout'] = str_pad(substr($row['cueout'], 0, 10), 10, '.0');

            //format original length
            $formatter = new LengthFormatter($row['orig_length']);
            $row['orig_length'] = $formatter->format();

            // XSS exploit prevention
            $row["track_title"] = htmlspecialchars($row["track_title"]);
            $row["creator"] = htmlspecialchars($row["creator"]);
        }

        return $rows;
    }

    /**
    * The database stores fades in 00:00:00 Time format with optional millisecond resolution .000000
    * but this isn't practical since fades shouldn't be very long usuall 1 second or less. This function
    * will normalize the fade so that it looks like 00.000000 to the user.
    **/
    public function normalizeFade($fade)
    {
        //First get rid of the first six characters 00:00: which will be added back later for db update
        $fade = substr($fade, 6);

        //Second add .000000 if the fade does't have milliseconds format already
        $dbFadeStrPos = strpos( $fade, '.' );
        if ($dbFadeStrPos === false) {
             $fade .= '.000000';
        } else {
            while (strlen($fade) < 9) {
                 $fade .= '0';
            }
        }

        //done, just need to set back the formated values
        return $fade;
    }

    // returns true/false and ids of dynamic blocks
    public function hasDynamicBlock()
    {
        $ids = $this->getIdsOfDynamicBlocks();
        if (count($ids) > 0) {
            return true;
        } else {
            return false;
        }
    }

    public function getIdsOfDynamicBlocks()
    {
        $sql = "SELECT bl.id FROM cc_playlistcontents as pc
                JOIN cc_block as bl ON pc.type=2 AND pc.block_id=bl.id AND bl.type='dynamic'
                WHERE playlist_id=:playlist_id AND pc.type=2";
        
        $result = Application_Common_Database::prepareAndExecute($sql, array(':playlist_id'=>$this->id));

        return $result;
    }

    //aggregate column on playlistcontents cliplength column.
    public function getLength()
    {
        if ($this->hasDynamicBlock()) {
            $ids = $this->getIdsOfDynamicBlocks();
            $length = $this->pl->getDbLength();
            foreach ($ids as $id) {
                $bl = new Application_Model_Block($id['id']);
                if ($bl->hasItemLimit()) {
                    return "N/A";
                }
            }
            $formatter = new LengthFormatter($length);

            return "~".$formatter->format();
        } else {
            return $this->pl->getDbLength();
        }
    }

    private function insertPlaylistElement($info)
    {
        $row = new CcPlaylistcontents();
        $row->setDbPlaylistId($this->id);
        $row->setDbPosition($info["pos"]);
        $row->setDbCliplength($info["cliplength"]);
        $row->setDbCuein($info["cuein"]);
        $row->setDbCueout($info["cueout"]);
        $row->setDbFadein(Application_Common_DateHelper::secondsToPlaylistTime($info["fadein"]));
        $row->setDbFadeout(Application_Common_DateHelper::secondsToPlaylistTime($info["fadeout"]));
        if ($info["ftype"] == "audioclip") {
            $row->setDbFileId($info["id"]);
            $row->setDbTrackOffset($info["crossfadeDuration"]);
            $type = 0;
        } elseif ($info["ftype"] == "stream") {
            $row->setDbStreamId($info["id"]);
            $type = 1;
        } elseif ($info["ftype"] == "block") {
            $row->setDbBlockId($info["id"]);
            $type = 2;
        }
        $row->setDbType($type);
        $row->save($this->con);
        // above save result update on cc_playlist table on length column.
        // but $this->pl doesn't get updated automatically
        // so we need to manually grab it again from DB so it has updated values
        // It is something to do FORMAT_ON_DEMAND( Lazy Loading )
        $this->pl = CcPlaylistQuery::create()->findPK($this->id);
    }

    /*
     *
     */
    private function buildEntry($p_item, $pos)
    {
        $objType = $p_item[1];
        $objId = $p_item[0];
        if ($objType == 'audioclip') {
            $obj = CcFilesQuery::create()->findPK($objId, $this->con);
        } elseif ($objType == "stream") {
            $obj = CcWebstreamQuery::create()->findPK($objId, $this->con);
        } elseif ($objType == "block") {
            $obj = CcBlockQuery::create()->findPK($objId, $this->con);
        } else {
            throw new Exception("Unknown file type");
        }

        if (isset($obj)) {
            if (($obj instanceof CcFiles && $obj->visible())
                || $obj instanceof CcWebstream ||
                $obj instanceof CcBlock) {

                $entry = $this->plItem;
                $entry["id"] = $obj->getDbId();
                $entry["pos"] = $pos;
                $entry["cliplength"] = $obj->getDbLength();
                
                if ($obj instanceof CcFiles && $obj) {
                	
                    $entry["cuein"] = isset($p_item['cuein']) ? 
                    		$p_item['cuein'] : $obj->getDbCuein();
                    
                    $entry["cueout"] = isset($p_item['cueout']) ? 
                    		$p_item['cueout'] : $obj->getDbCueout();

                    $cue_in = isset($p_item['cueInSec']) ? 
                    		$p_item['cueInSec'] : Application_Common_DateHelper::calculateLengthInSeconds($entry['cuein']);
                    
                    $cue_out = isset($p_item['cueOutSec']) ? 
                    		$p_item['cueOutSec'] : Application_Common_DateHelper::calculateLengthInSeconds($entry['cueout']);
                    
                    $entry["cliplength"] = isset($p_item['length']) ? 
                    		$p_item['length'] : Application_Common_DateHelper::secondsToPlaylistTime($cue_out-$cue_in);
                } 
                elseif ($obj instanceof CcWebstream && $obj) {
                    $entry["cuein"] = "00:00:00";
                    $entry["cueout"] = $entry["cliplength"];
                }
                $entry["ftype"] = $objType;
                
                $entry["fadein"] = isset($p_item['fadein']) ?
                	$p_item['fadein'] : $entry["fadein"];
                
                $entry["fadeout"] = isset($p_item['fadeout']) ?
                	$p_item['fadeout'] : $entry["fadeout"];
            }

            return $entry;
        } 
        else {
            throw new Exception("trying to add a object that does not exist.");
        }
    }

    /*
     * @param array $p_items
     *     an array of audioclips to add to the playlist
     * @param int|null $p_afterItem
     *     item which to add the new items after in the playlist, null if added to the end.
     * @param string (before|after) $addAfter
     *      whether to add the clips before or after the selected item.
     */
    public function addAudioClips($p_items, $p_afterItem=null, $addType = 'after')
    {
        $this->con->beginTransaction();
        $contentsToUpdate = array();

        try {

            if (is_numeric($p_afterItem)) {
               
                $afterItem = CcPlaylistcontentsQuery::create()->findPK($p_afterItem);
                $index = $afterItem->getDbPosition();

                $pos = ($addType == 'after') ? $index + 1 : $index;

                $contentsToUpdate = CcPlaylistcontentsQuery::create()
                    ->filterByDbPlaylistId($this->id)
                    ->filterByDbPosition($pos, Criteria::GREATER_EQUAL)
                    ->orderByDbPosition()
                    ->find($this->con);

            } else {

                //add to the end of the playlist
                if ($addType == 'after') {
                    $pos = $this->getSize();
                }
                //add to the beginning of the playlist.
                else {
                    $pos = 0;

                    $contentsToUpdate = CcPlaylistcontentsQuery::create()
                        ->filterByDbPlaylistId($this->id)
                        ->orderByDbPosition()
                        ->find($this->con);
                }

                $contentsToUpdate = CcPlaylistcontentsQuery::create()
                    ->filterByDbPlaylistId($this->id)
                    ->filterByDbPosition($pos, Criteria::GREATER_EQUAL)
                    ->orderByDbPosition()
                    ->find($this->con);

            }

            foreach ($p_items as $ac) {
                $res = $this->insertPlaylistElement($this->buildEntry($ac, $pos));
                // update is_playlist flag in cc_files to indicate the
                // file belongs to a playlist or block (in this case a playlist)
                if ($ac[1] == "audioclip") {
                    $db_file = CcFilesQuery::create()->findPk($ac[0], $this->con);
                    $db_file->setDbIsPlaylist(true)->save($this->con);
                }
                
                $pos = $pos + 1;
            }

            //reset the positions of the remaining items.
            for ($i = 0; $i < count($contentsToUpdate); $i++) {
                $contentsToUpdate[$i]->setDbPosition($pos);
                $contentsToUpdate[$i]->save($this->con);
                $pos = $pos + 1;
            }

            $this->pl->setDbMtime(new DateTime("now", new DateTimeZone("UTC")));
            $this->pl->save($this->con);

            $this->con->commit();
        } catch (Exception $e) {
            $this->con->rollback();
            throw $e;
        }
    }

    /**
     * Move audioClip to the new position in the playlist
     *
     * @param array $p_items
     *      array of unique ids of the selected items
     * @param int $p_afterItem
     *      unique id of the item to move the clip after
     */
    public function moveAudioClips($p_items, $p_afterItem=NULL)
    {
        $this->con->beginTransaction();

        try {

            $contentsToMove = CcPlaylistcontentsQuery::create()
                    ->filterByDbId($p_items, Criteria::IN)
                    ->orderByDbPosition()
                    ->find($this->con);

            $otherContent = CcPlaylistcontentsQuery::create()
                    ->filterByDbId($p_items, Criteria::NOT_IN)
                    ->filterByDbPlaylistId($this->id)
                    ->orderByDbPosition()
                    ->find($this->con);

            $pos = 0;
            //moving items to beginning of the playlist.
            if (is_null($p_afterItem)) {
                Logging::info("moving items to beginning of playlist");

                foreach ($contentsToMove as $item) {
                    Logging::info("item {$item->getDbId()} to pos {$pos}");
                    $item->setDbPosition($pos);
                    $item->save($this->con);
                    $pos = $pos + 1;
                }
                foreach ($otherContent as $item) {
                    Logging::info("item {$item->getDbId()} to pos {$pos}");
                    $item->setDbPosition($pos);
                    $item->save($this->con);
                    $pos = $pos + 1;
                }
            } else {
                Logging::info("moving items after {$p_afterItem}");

                foreach ($otherContent as $item) {
                    Logging::info("item {$item->getDbId()} to pos {$pos}");
                    $item->setDbPosition($pos);
                    $item->save($this->con);
                    $pos = $pos + 1;

                    if ($item->getDbId() == $p_afterItem) {
                        foreach ($contentsToMove as $move) {
                            Logging::info("item {$move->getDbId()} to pos {$pos}");
                            $move->setDbPosition($pos);
                            $move->save($this->con);
                            $pos = $pos + 1;
                        }
                    }
                }
            }

            $this->con->commit();
        } catch (Exception $e) {
            $this->con->rollback();
            throw $e;
        }

        $this->pl = CcPlaylistQuery::create()->findPK($this->id);
        $this->pl->setDbMtime(new DateTime("now", new DateTimeZone("UTC")));
        $this->pl->save($this->con);
    }

    /**
     * Remove audioClip from playlist
     *
     * @param array $p_items
     *         array of unique item ids to remove from the playlist..
     */
    public function delAudioClips($p_items)
    {

        $this->con->beginTransaction();

        try {

            // we need to get the file id of the item we are deleting
            // before the item gets deleted from the playlist
            $itemsToDelete = CcPlaylistcontentsQuery::create()
                ->filterByPrimaryKeys($p_items)
                ->filterByDbFileId(null, Criteria::NOT_EQUAL)
                ->find($this->con);

            CcPlaylistcontentsQuery::create()
                ->findPKs($p_items)
                ->delete($this->con);

            // now that the items have been deleted we can update the
            // is_playlist flag in cc_files
            Application_Model_StoredFile::setIsPlaylist($itemsToDelete, 'playlist', false);
            
            $contents = CcPlaylistcontentsQuery::create()
                ->filterByDbPlaylistId($this->id)
                ->orderByDbPosition()
                ->find($this->con);

            //reset the positions of the remaining items.
            for ($i = 0; $i < count($contents); $i++) {
                $contents[$i]->setDbPosition($i);
                $contents[$i]->save($this->con);
            }

            $this->pl->setDbMtime(new DateTime("now", new DateTimeZone("UTC")));
            $this->pl->save($this->con);

            $this->con->commit();
        } catch (Exception $e) {
            $this->con->rollback();
            throw $e;
        }
    }

    public function getFadeInfo($pos)
    {
        $row = CcPlaylistcontentsQuery::create()
            ->joinWith(CcFilesPeer::OM_CLASS)
            ->filterByDbPlaylistId($this->id)
            ->filterByDbPosition($pos)
            ->findOne();

        if (!$row) {
            return NULL;
        }
        //Propel returns values in form 00.000000 format which is for only seconds.
        //We only want to display 1 decimal
        $fadeIn = substr($row->getDbFadein(), 0, 4);
        $fadeOut = substr($row->getDbFadeout(), 0, 4);

        return array($fadeIn, $fadeOut);
    }
    
    /*
     * create a crossfade from item in cc_playlist_contents with $id1 to item $id2.
     * 
     * $fadeOut length of fade out in seconds if $id1
     * $fadeIn length of fade in in seconds of $id2
     * $offset time in seconds from end of $id1 that $id2 will begin to play.
     */
    public function createCrossfade($id1, $fadeOut, $id2, $fadeIn, $offset)
    {
    	$this->con->beginTransaction();
    	
    	if (!isset($offset)) {
    		$offset = Application_Model_Preference::GetDefaultCrossfadeDuration();	
    	}
    	
    	try {
    		if (isset($id1)) {
    			$this->changeFadeInfo($id1, null, $fadeOut);
    		}
    		if (isset($id2)) {
    			$this->changeFadeInfo($id2, $fadeIn, null, $offset);
    		}
    		
    		$this->con->commit();
    		
    	} catch (Exception $e) {
            $this->con->rollback();
            throw $e;
        }	
    }

    /**
     * Change fadeIn and fadeOut values for playlist Element
     *
     * @param int $id
     *         id of audioclip in playlist contents table.
     * @param string $fadeIn
     *         new value in ss.ssssss or extent format
     * @param string $fadeOut
     *         new value in ss.ssssss or extent format
     * @return boolean
     */
    public function changeFadeInfo($id, $fadeIn, $fadeOut, $offset=null)
    {
        //See issue CC-2065, pad the fadeIn and fadeOut so that it is TIME compatable with the DB schema
        //For the top level PlayList either fadeIn or fadeOut will sometimes be Null so need a gaurd against
       //setting it to nonNull for checks down below
        $fadeIn = $fadeIn?'00:00:'.$fadeIn:$fadeIn;
        $fadeOut = $fadeOut?'00:00:'.$fadeOut:$fadeOut;
        
        $this->con->beginTransaction();

        try {
            $row = CcPlaylistcontentsQuery::create()->findPK($id);

            if (is_null($row)) {
                throw new Exception("Playlist item does not exist.");
            }

            $clipLength = $row->getDbCliplength();
            if (!is_null($fadeIn)) {

                $sql = "SELECT :fadein::INTERVAL > INTERVAL '{$clipLength}'";
                if (Application_Common_Database::prepareAndExecute($sql, array(':fadein'=>$fadeIn), 'column')) {
                    //"Fade In can't be larger than overall playlength.";
                    $fadeIn = $clipLength;
                }
                $row->setDbFadein($fadeIn);
                
                if (!is_null($offset)) {
                	$row->setDbTrackOffset($offset);
                	$row->save($this->con);
                }
            }
            if (!is_null($fadeOut)) {

                $sql = "SELECT :fadeout::INTERVAL > INTERVAL '{$clipLength}'";
                if (Application_Common_Database::prepareAndExecute($sql, array(':fadeout'=>$fadeOut), 'column')) {
                    //Fade Out can't be larger than overall playlength.";
                    $fadeOut = $clipLength;
                }
                $row->setDbFadeout($fadeOut);
            }
            $this->pl->setDbMtime(new DateTime("now", new DateTimeZone("UTC")));
            $this->pl->save($this->con);
            
            $this->con->commit();
        } catch (Exception $e) {
            $this->con->rollback();
            throw $e;
        }

        return array("fadeIn" => $fadeIn, "fadeOut" => $fadeOut);
    }

    public function setfades($fadein, $fadeout)
    {
        if (isset($fadein)) {
            Logging::info("Setting playlist fade in {$fadein}");
            $row = CcPlaylistcontentsQuery::create()
                ->filterByDbPlaylistId($this->id)
                ->filterByDbPosition(0)
                ->findOne($this->con);

            $this->changeFadeInfo($row->getDbId(), $fadein, null);
        }
        if (isset($fadeout)) {
            Logging::info("Setting playlist fade out {$fadeout}");
            $row = CcPlaylistcontentsQuery::create()
                ->filterByDbPlaylistId($this->id)
                ->filterByDbPosition($this->getSize()-1)
                ->findOne($this->con);

            $this->changeFadeInfo($row->getDbId(), null, $fadeout);
        }
    }

    /**
     * Change cueIn/cueOut values for playlist element
     *
     * @param int $pos
     *         position of audioclip in playlist
     * @param string $cueIn
     *         new value in ss.ssssss or extent format
     * @param string $cueOut
     *         new value in ss.ssssss or extent format
     * @return boolean or pear error object
     */
    public function changeClipLength($id, $cueIn, $cueOut)
    {
        $this->con->beginTransaction();

        $errArray= array();

        try {
            if (is_null($cueIn) && is_null($cueOut)) {
                $errArray["error"] = _("Cue in and cue out are null.");
                $errArray["type"] = self::CUE_ALL_ERROR;

                return $errArray;
            }

            $row = CcPlaylistcontentsQuery::create()
                ->joinWith(CcFilesPeer::OM_CLASS)
                ->filterByPrimaryKey($id)
                ->findOne($this->con);

            if (is_null($row)) {
                throw new Exception("Playlist item does not exist.");
            }

            $oldCueIn = $row->getDBCuein();
            $oldCueOut = $row->getDbCueout();
            $fadeIn = $row->getDbFadein();
            $fadeOut = $row->getDbFadeout();

            $file = $row->getCcFiles($this->con);
            $origLength = $file->getDbLength();

            if (!is_null($cueIn) && !is_null($cueOut)) {

                if ($cueOut === "") {
                    $cueOut = $origLength;
                }

                $sql = "SELECT :cueIn::INTERVAL > :cueOut::INTERVAL";
                if (Application_Common_Database::prepareAndExecute($sql, array(':cueIn'=>$cueIn, ':cueOut'=>$cueOut), 'column')) {
                    $errArray["error"] = _("Can't set cue in to be larger than cue out.");
                    $errArray["type"] = self::CUE_IN_ERROR;

                    return $errArray;
                }

                $sql = "SELECT :cueOut::INTERVAL > :origLength::INTERVAL";
                if (Application_Common_Database::prepareAndExecute($sql, array(':cueOut'=>$cueOut, ':origLength'=>$origLength), 'column')) {
                    $errArray["error"] = _("Can't set cue out to be greater than file length.");
                    $errArray["type"] = self::CUE_OUT_ERROR;

                    return $errArray;
                }

                $sql = "SELECT :cueOut::INTERVAL - :cueIn::INTERVAL";
                $cliplength = Application_Common_Database::prepareAndExecute($sql, array(':cueOut'=>$cueOut, ':cueIn'=>$cueIn), 'column');

                $row->setDbCuein($cueIn);
                $row->setDbCueout($cueOut);
                $row->setDBCliplength($cliplength);

            } elseif (!is_null($cueIn)) {

                $sql = "SELECT :cueIn::INTERVAL > :oldCueOut::INTERVAL";
                if (Application_Common_Database::prepareAndExecute($sql, array(':cueIn'=>$cueIn, ':oldCueOut'=>$oldCueOut), 'column')) {
                    $errArray["error"] = _("Can't set cue in to be larger than cue out.");
                    $errArray["type"] = self::CUE_IN_ERROR;

                    return $errArray;
                }

                $sql = "SELECT :oldCueOut::INTERVAL - :cueIn::INTERVAL";
                $cliplength = Application_Common_Database::prepareAndExecute($sql, array(':cueIn'=>$cueIn, ':oldCueOut'=>$oldCueOut), 'column');

                $row->setDbCuein($cueIn);
                $row->setDBCliplength($cliplength);
            } elseif (!is_null($cueOut)) {

                if ($cueOut === "") {
                    $cueOut = $origLength;
                }

                $sql = "SELECT :cueOut::INTERVAL < :oldCueIn::INTERVAL";
                if (Application_Common_Database::prepareAndExecute($sql, array(':cueOut'=>$cueOut, ':oldCueIn'=>$oldCueIn), 'column')) {
                    $errArray["error"] = _("Can't set cue out to be smaller than cue in.");
                    $errArray["type"] = self::CUE_OUT_ERROR;

                    return $errArray;
                }

                $sql = "SELECT :cueOut::INTERVAL > :origLength::INTERVAL";
                if (Application_Common_Database::prepareAndExecute($sql, array(':cueOut'=>$cueOut, ':origLength'=>$origLength), 'column')) {
                    $errArray["error"] = _("Can't set cue out to be greater than file length.");
                    $errArray["type"] = self::CUE_OUT_ERROR;

                    return $errArray;
                }

                $sql = "SELECT :cueOut::INTERVAL - :oldCueIn::INTERVAL";
                $cliplength = Application_Common_Database::prepareAndExecute($sql, array(':cueOut'=>$cueOut, ':oldCueIn'=>$oldCueIn), 'column');

                $row->setDbCueout($cueOut);
                $row->setDBCliplength($cliplength);
            }

            $cliplength = $row->getDbCliplength();

            $sql = "SELECT :fadeIn::INTERVAL > :cliplength::INTERVAL";
            if (Application_Common_Database::prepareAndExecute($sql, array(':fadeIn'=>$fadeIn, ':cliplength'=>$cliplength), 'column')) {
                $fadeIn = $cliplength;
                $row->setDbFadein($fadeIn);
            }

            $sql = "SELECT :fadeOut::INTERVAL > :cliplength::INTERVAL";
            if (Application_Common_Database::prepareAndExecute($sql, array(':fadeOut'=>$fadeOut, ':cliplength'=>$cliplength), 'column')) {
                $fadeOut = $cliplength;
                $row->setDbFadein($fadeOut);
            }

            $row->save($this->con);
            $this->pl->setDbMtime(new DateTime("now", new DateTimeZone("UTC")));
            $this->pl->save($this->con);

            $this->con->commit();
        } catch (Exception $e) {
            $this->con->rollback();
            throw $e;
        }

        return array("cliplength"=> $cliplength, "cueIn"=> $cueIn, "cueOut"=> $cueOut, "length"=> $this->getLength(),
                        "fadeIn"=> $fadeIn, "fadeOut"=> $fadeOut);
    }

    public function getAllPLMetaData()
    {
        $categories = $this->categories;
        $md = array();

        foreach ($categories as $key => $val) {
            $method = 'get' . $val;
            $md[$key] = $this->$method();
        }

        return $md;
    }

    public function getMetaData($category)
    {
        $cat = $this->categories[$category];
        $method = 'get' . $cat;

        return $this->$method();
    }

    public function setMetadata($category, $value)
    {
        $cat = $this->categories[$category];

        $method = 'set' . $cat;
        $this->$method($value);
    }

    public static function getPlaylistCount()
    {
        $sql = 'SELECT count(*) as cnt FROM cc_playlist';

        return Application_Common_Database::prepareAndExecute($sql, array(),
            Application_Common_Database::COLUMN);
    }

    /**
     * Delete the file from all playlists.
     * @param string $p_fileId
     */
    public static function DeleteFileFromAllPlaylists($p_fileId)
    {
        CcPlaylistcontentsQuery::create()->filterByDbFileId($p_fileId)->delete();
    }

    /**
     * Delete playlists that match the ids..
     * @param array $p_ids
     */
    public static function deletePlaylists($p_ids, $p_userId)
    {
        $userInfo = Zend_Auth::getInstance()->getStorage()->read();
        $user = new Application_Model_User($userInfo->id);
        $isAdminOrPM = $user->isUserType(array(UTYPE_SUPERADMIN, UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER));

        // get only the files from the playlists
        // we are about to delete
        $itemsToDelete = CcPlaylistcontentsQuery::create()
            ->filterByDbPlaylistId($p_ids)
            ->filterByDbFileId(null, Criteria::NOT_EQUAL)
            ->find();

        $updateIsPlaylistFlag = false;

        if (!$isAdminOrPM) {
            $leftOver = self::playlistsNotOwnedByUser($p_ids, $p_userId);
            if (count($leftOver) == 0) {
                CcPlaylistQuery::create()->findPKs($p_ids)->delete();
                $updateIsPlaylistFlag = true;
                
            } else {
                throw new PlaylistNoPermissionException;
            }
        } else {
            CcPlaylistQuery::create()->findPKs($p_ids)->delete();
            $updateIsPlaylistFlag = true;
        }
        
        if ($updateIsPlaylistFlag) {
            // update is_playlist flag in cc_files
            Application_Model_StoredFile::setIsPlaylist(
                $itemsToDelete,
                'playlist',
                false
            );
        }
    }

    // This function returns that are not owen by $p_user_id among $p_ids
    private static function playlistsNotOwnedByUser($p_ids, $p_userId)
    {
        $ownedByUser = CcPlaylistQuery::create()->filterByDbCreatorId($p_userId)->find()->getData();
        $selectedPls = $p_ids;
        $ownedPls = array();
        foreach ($ownedByUser as $pl) {
            if (in_array($pl->getDbId(), $selectedPls)) {
                $ownedPls[] = $pl->getDbId();
            }
        }

        $leftOvers = array_diff($selectedPls, $ownedPls);

        return $leftOvers;
    }

    /**
     * Delete all files from playlist
     * @param int $p_playlistId
     */
    public function deleteAllFilesFromPlaylist()
    {
        // get only the files from the playlist
        // we are about to clear out
        $itemsToDelete = CcPlaylistcontentsQuery::create()
            ->filterByDbPlaylistId($this->id)
            ->filterByDbFileId(null, Criteria::NOT_EQUAL)
            ->find();

        CcPlaylistcontentsQuery::create()->findByDbPlaylistId($this->id)->delete();

        // update is_playlist flag in cc_files
        Application_Model_StoredFile::setIsPlaylist(
            $itemsToDelete,
            'playlist',
            false
        );

        $this->pl->setDbMtime(new DateTime("now", new DateTimeZone("UTC")));
        $this->pl->save($this->con);
        $this->con->commit();
    }
    
    public function shuffle()
    {
        $sql = <<<SQL
SELECT max(position) from cc_playlistcontents WHERE playlist_id=:p1
SQL;
        $out = Application_Common_Database::prepareAndExecute($sql, array("p1"=>$this->id));
        $maxPosition = $out[0]['max'];
        
        $map = range(0, $maxPosition);
        shuffle($map);
        
        $currentPos = implode(',', array_keys($map));
        $sql = "UPDATE cc_playlistcontents SET position = CASE position ";
        foreach ($map as $current => $after) {
            $sql .= sprintf("WHEN %d THEN %d ", $current, $after);
        }
        $sql .= "END WHERE position IN ($currentPos) and playlist_id=:p1";
        
        Application_Common_Database::prepareAndExecute($sql, array("p1"=>$this->id));
        return array('result' => 0);
    }

    public static function getAllPlaylistFiles()
    {
        $sql = <<<SQL
SELECT distinct(file_id)
FROM cc_playlistcontents
WHERE file_id is not null
SQL;
        $files = Application_Common_Database::prepareAndExecute($sql);
        $real_files = array();
        foreach ($files as $f) {
            $real_files[] = $f['file_id'];
        }
        return $real_files;
    }

    public static function getAllPlaylistStreams()
    {
        $sql = <<<SQL
SELECT distinct(stream_id)
FROM cc_playlistcontents
WHERE stream_id is not null
SQL;
        $streams = Application_Common_Database::prepareAndExecute($sql);
        $real_streams = array();
        foreach ($streams as $s) {
            $real_streams[] = $s['stream_id'];
        }
        return $real_streams;
    }

    /** Find out if a playlist contains any files that have been deleted from disk.
     *  This function relies on the "file_exists" column in the database being accurate and true,
     *  which it should.
     * @return boolean true if there are missing files in this playlist, false otherwise.
     */
    public function containsMissingFiles()
    {
        $playlistContents = $this->pl->getCcPlaylistcontentss("type = 0"); //type=0 is only files, not other types of media
        
        //Slightly faster than the other Propel version below (this one does an INNER JOIN):
        $missingFiles = CcFilesQuery::create()
                            ->join("CcFiles.CcPlaylistcontents")
                            ->where("CcPlaylistcontents.DbPlaylistId = ?", $this->pl->getDbId()) 
                            ->where("CcFiles.DbFileExists = ?", 'false')
                            ->find();
        
        //Nicer Propel version but slightly slower because it generates a LEFT JOIN:
        /*
        $missingFiles = CcPlaylistcontentsQuery::create()
        ->filterByDbPlaylistId($this->pl->getDbId())
        ->useCcFilesQuery()
        ->filterByDbFileExists(false)
        ->endUse()
        ->find();
        */
        
        if (!$missingFiles->isEmpty())
        {
            return true;
        }
        
        return false;        
    }
    
    
} // class Playlist

class PlaylistNotFoundException extends Exception {}
class PlaylistNoPermissionException extends Exception {}
class PlaylistOutDatedException extends Exception {}
