<?php

class Application_Service_SchedulerService
{
    private $con;
    private $fileInfo = [
        'id' => '',
        'cliplength' => '',
        'cuein' => '00:00:00',
        'cueout' => '00:00:00',
        'fadein' => '00:00:00',
        'fadeout' => '00:00:00',
        'sched_id' => null,
        'type' => 0, // default type of '0' to represent files. type '1' represents a webstream
    ];

    private $epochNow;
    private $nowDT;
    private $currentUser;
    private $checkUserPermissions = true;

    public function __construct()
    {
        $this->con = Propel::getConnection(CcSchedulePeer::DATABASE_NAME);

        // subtracting one because sometimes when we cancel a track, we set its end time
        // to epochNow and then send the new schedule to pypo. Sometimes the currently cancelled
        // track can still be included in the new schedule because it may have a few ms left to play.
        // subtracting 1 second from epochNow resolves this issue.
        $this->epochNow = microtime(true) - 1;
        $this->nowDT = DateTime::createFromFormat('U.u', $this->epochNow, new DateTimeZone('UTC'));

        if ($this->nowDT === false) {
            // DateTime::createFromFormat does not support millisecond string formatting in PHP 5.3.2 (Ubuntu 10.04).
            // In PHP 5.3.3 (Ubuntu 10.10), this has been fixed.
            $this->nowDT = DateTime::createFromFormat('U', time(), new DateTimeZone('UTC'));
        }

        $user_service = new Application_Service_UserService();
        $this->currentUser = $user_service->getCurrentUser();
    }

    /**
     * Applies the show start difference to any scheduled items.
     *
     * @param       $diff        (integer, difference between unix epoch in seconds)
     * @param mixed $instanceIds
     */
    public static function updateScheduleStartTime($instanceIds, $diff)
    {
        $con = Propel::getConnection();
        if (count($instanceIds) > 0) {
            $showIdList = implode(',', $instanceIds);

            $ccSchedules = CcScheduleQuery::create()
                ->filterByDbInstanceId($instanceIds, Criteria::IN)
                ->find($con);

            $interval = new DateInterval('PT' . abs($diff) . 'S');
            if ($diff < 0) {
                $interval->invert = 1;
            }
            foreach ($ccSchedules as $ccSchedule) {
                $start = $ccSchedule->getDbStarts(null);
                $newStart = $start->add($interval);
                $end = $ccSchedule->getDbEnds(null);
                $newEnd = $end->add($interval);
                $ccSchedule
                    ->setDbStarts($newStart)
                    ->setDbEnds($newEnd)
                    ->save($con);
            }
        }
    }

    /**
     * Removes any time gaps in shows.
     *
     * @param array $schedIds schedule ids to exclude
     * @param mixed $showId
     */
    public function removeGaps($showId, $schedIds = null)
    {
        $ccShowInstances = CcShowInstancesQuery::create()->filterByDbShowId($showId)->find();

        foreach ($ccShowInstances as $instance) {
            Logging::info('Removing gaps from show instance #' . $instance->getDbId());
            // DateTime object
            $itemStart = $instance->getDbStarts(null);

            $ccScheduleItems = CcScheduleQuery::create()
                ->filterByDbInstanceId($instance->getDbId())
                ->filterByDbId($schedIds, Criteria::NOT_IN)
                ->orderByDbStarts()
                ->find();

            foreach ($ccScheduleItems as $ccSchedule) {
                // DateTime object
                $itemEnd = $this->findEndTime($itemStart, $ccSchedule->getDbClipLength());

                $ccSchedule->setDbStarts($itemStart)
                    ->setDbEnds($itemEnd);

                $itemStart = $itemEnd;
            }
            $ccScheduleItems->save();
        }
    }

    /**
     * Enter description here ...
     *
     * @param DateTime $instanceStart
     * @param string   $clipLength
     */
    private static function findEndTime($instanceStart, $clipLength)
    {
        $startEpoch = $instanceStart->format('U.u');
        $durationSeconds = Application_Common_DateHelper::playlistTimeToSeconds($clipLength);

        // add two float numbers to 6 subsecond precision
        // DateTime::createFromFormat("U.u") will have a problem if there is no decimal in the resulting number.
        $endEpoch = bcadd($startEpoch, (string) $durationSeconds, 6);

        $dt = DateTime::createFromFormat('U.u', $endEpoch, new DateTimeZone('UTC'));

        if ($dt === false) {
            // PHP 5.3.2 problem
            $dt = DateTime::createFromFormat('U', intval($endEpoch), new DateTimeZone('UTC'));
        }

        return $dt;
    }

    private static function findTimeDifference($p_startDT, $p_seconds)
    {
        $startEpoch = $p_startDT->format('U.u');

        // add two float numbers to 6 subsecond precision
        // DateTime::createFromFormat("U.u") will have a problem if there is no decimal in the resulting number.
        $newEpoch = bcsub($startEpoch, (string) $p_seconds, 6);

        $dt = DateTime::createFromFormat('U.u', $newEpoch, new DateTimeZone('UTC'));

        if ($dt === false) {
            // PHP 5.3.2 problem
            $dt = DateTime::createFromFormat('U', intval($newEpoch), new DateTimeZone('UTC'));
        }

        return $dt;
    }

    /**
     * Gets a copy of the linked show's schedule from cc_schedule table.
     *
     * If $instanceId is not null, we use that variable to grab the linked
     * show's schedule from cc_schedule table. (This is likely to be the case
     * if a user has edited a show and changed it from un-linked to linked, in
     * which case we copy the show content from the show instance that was clicked
     * on to edit the show in the calendar.) Otherwise the schedule is taken
     * from the most recent show instance that existed before new show
     * instances were created. (This is likely to be the case when a user edits a
     * show and a new repeat show day is added (i.e. mondays), or moves forward in the
     * calendar triggering show creation)
     *
     * @param int   $showId
     * @param array $instancsIdsToFill
     * @param int   $instanceId
     */
    private static function getLinkedShowSchedule($showId, $instancsIdsToFill, $instanceId)
    {
        $con = Propel::getConnection();

        if (is_null($instanceId)) {
            $showsPopulatedUntil = Application_Model_Preference::GetShowsPopulatedUntil();

            $showInstanceWithMostRecentSchedule = CcShowInstancesQuery::create()
                ->filterByDbShowId($showId)
                ->filterByDbStarts($showsPopulatedUntil->format(DEFAULT_TIMESTAMP_FORMAT), Criteria::LESS_THAN)
                ->filterByDbId($instancsIdsToFill, Criteria::NOT_IN)
                ->orderByDbStarts(Criteria::DESC)
                ->limit(1)
                ->findOne();

            if (is_null($showInstanceWithMostRecentSchedule)) {
                return null;
            }
            $instanceId = $showInstanceWithMostRecentSchedule->getDbId();
        }

        $linkedShowSchedule_sql = $con->prepare(
            'select * from cc_schedule where instance_id = :instance_id ' .
                'order by starts'
        );
        $linkedShowSchedule_sql->bindParam(':instance_id', $instanceId);
        $linkedShowSchedule_sql->execute();

        return $linkedShowSchedule_sql->fetchAll();
    }

    /**
     * This function gets called after new linked show_instances are created, or after
     * a show has been edited and went from being un-linked to linked.
     * It fills the new show instances' schedules.
     *
     * @param CcShow_type $ccShow
     * @param array       $instanceIdsToFill ids of the new linked cc_show_instances that
     *                                       need their schedules filled
     * @param null|mixed  $instanceId
     */
    public static function fillLinkedInstances($ccShow, $instanceIdsToFill, $instanceId = null)
    {
        // Get the "template" schedule for the linked show (contents of the linked show) that will be
        // copied into to all the new show instances.
        $linkedShowSchedule = self::getLinkedShowSchedule($ccShow->getDbId(), $instanceIdsToFill, $instanceId);

        // get time_filled so we can update cc_show_instances
        if (!empty($linkedShowSchedule)) {
            $timeFilled_sql = 'SELECT time_filled FROM cc_show_instances ' .
                "WHERE id = {$linkedShowSchedule[0]['instance_id']}";
            $timeFilled = Application_Common_Database::prepareAndExecute(
                $timeFilled_sql,
                [],
                Application_Common_Database::COLUMN
            );
        } else {
            // We probably shouldn't return here because the code below will
            // set this on each empty show instance...
            $timeFilled = '00:00:00';
        }

        $values = [];

        $con = Propel::getConnection();

        // Here we begin to fill the new show instances (as specified by $instanceIdsToFill)
        // with content from $linkedShowSchedule.
        try {
            $con->beginTransaction();

            if (!empty($linkedShowSchedule)) {
                foreach ($instanceIdsToFill as $id) {
                    // Start by clearing the show instance that needs to be filling. This ensure
                    // we're not going to get in trouble in case there's an programming error somewhere else.
                    self::clearShowInstanceContents($id);

                    // Now fill the show instance with the same content that $linkedShowSchedule has.
                    $instanceStart_sql = 'SELECT starts FROM cc_show_instances ' .
                        "WHERE id = {$id} " . 'ORDER BY starts';

                    // What's tricky here is that when we copy the content, we have to adjust
                    // the start and end times of each track so they're inside the new show instance's time slot.
                    $nextStartDT = new DateTime(
                        Application_Common_Database::prepareAndExecute(
                            $instanceStart_sql,
                            [],
                            Application_Common_Database::COLUMN
                        ),
                        new DateTimeZone('UTC')
                    );

                    $defaultCrossfadeDuration = Application_Model_Preference::GetDefaultCrossfadeDuration();
                    unset($values);
                    $values = [];
                    foreach ($linkedShowSchedule as $item) {
                        $endTimeDT = self::findEndTime(
                            $nextStartDT,
                            $item['clip_length']
                        );

                        if (is_null($item['file_id'])) {
                            $item['file_id'] = 'null';
                        }
                        if (is_null($item['stream_id'])) {
                            $item['stream_id'] = 'null';
                        }

                        $values[] = '(' . "'{$nextStartDT->format(DEFAULT_TIMESTAMP_FORMAT)}', " .
                            "'{$endTimeDT->format(DEFAULT_TIMESTAMP_FORMAT)}', " .
                            "'{$item['clip_length']}', " .
                            "'{$item['fade_in']}', '{$item['fade_out']}', " .
                            "'{$item['cue_in']}', '{$item['cue_out']}', " .
                            "{$item['file_id']}, {$item['stream_id']}, " .
                            "{$id}, {$item['position']})";

                        $nextStartDT = self::findTimeDifference(
                            $endTimeDT,
                            $defaultCrossfadeDuration
                        );
                    } // foreach show item

                    if (!empty($values)) {
                        $insert_sql = 'INSERT INTO cc_schedule (starts, ends, ' .
                            'clip_length, fade_in, fade_out, cue_in, cue_out, ' .
                            'file_id, stream_id, instance_id, position)  VALUES ' .
                            implode(',', $values);
                        Application_Common_Database::prepareAndExecute(
                            $insert_sql,
                            [],
                            Application_Common_Database::EXECUTE
                        );
                    }

                    // update cc_schedule status column
                    $instance = CcShowInstancesQuery::create()->findPk($id);
                    $instance->updateScheduleStatus($con);
                } // foreach linked instance
            }

            // update time_filled and last_scheduled in cc_show_instances
            $now = gmdate(DEFAULT_TIMESTAMP_FORMAT);
            $whereClause = new Criteria();
            $whereClause->add(CcShowInstancesPeer::ID, $instanceIdsToFill, Criteria::IN);

            $updateClause = new Criteria();
            $updateClause->add(CcShowInstancesPeer::TIME_FILLED, $timeFilled);
            $updateClause->add(CcShowInstancesPeer::LAST_SCHEDULED, $now);

            BasePeer::doUpdate($whereClause, $updateClause, $con);

            $con->commit();
            Logging::info('finished fill');
        } catch (Exception $e) {
            $con->rollback();
            Logging::info('Error filling linked shows: ' . $e->getMessage());

            exit;
        }
    }

    public static function fillPreservedLinkedShowContent($ccShow, $showStamp)
    {
        $item = $showStamp->getFirst();
        $timeFilled = $item->getCcShowInstances()->getDbTimeFilled();

        foreach ($ccShow->getCcShowInstancess() as $ccShowInstance) {
            $ccSchedules = CcScheduleQuery::create()
                ->filterByDbInstanceId($ccShowInstance->getDbId())
                ->find();

            if ($ccSchedules->isEmpty()) {
                $nextStartDT = $ccShowInstance->getDbStarts(null);

                foreach ($showStamp as $item) {
                    $endTimeDT = self::findEndTime($nextStartDT, $item->getDbClipLength());

                    $ccSchedule = new CcSchedule();
                    $ccSchedule
                        ->setDbStarts($nextStartDT)
                        ->setDbEnds($endTimeDT)
                        ->setDbFileId($item->getDbFileId())
                        ->setDbStreamId($item->getDbStreamId())
                        ->setDbClipLength($item->getDbClipLength())
                        ->setDbFadeIn($item->getDbFadeIn())
                        ->setDbFadeOut($item->getDbFadeOut())
                        ->setDbCuein($item->getDbCueIn())
                        ->setDbCueOut($item->getDbCueOut())
                        ->setDbInstanceId($ccShowInstance->getDbId())
                        ->setDbPosition($item->getDbPosition())
                        ->save();

                    $nextStartDT = self::findTimeDifference(
                        $endTimeDT,
                        Application_Model_Preference::GetDefaultCrossfadeDuration()
                    );
                } // foreach show item

                $ccShowInstance
                    ->setDbTimeFilled($timeFilled)
                    ->setDbLastScheduled(gmdate(DEFAULT_TIMESTAMP_FORMAT))
                    ->save();
            }
        }
    }

    /** Clears a show instance's schedule (which is actually clearing cc_schedule during that show instance's time slot.) */
    private static function clearShowInstanceContents($instanceId)
    {
        // Application_Common_Database::prepareAndExecute($delete_sql, array(), Application_Common_Database::EXECUTE);
        $con = Propel::getConnection();
        $query = $con->prepare('DELETE FROM cc_schedule WHERE instance_id = :instance_id');
        $query->bindParam(':instance_id', $instanceId);
        $query->execute();
    }

    /*
    private static function replaceInstanceContentCheck($currentShowStamp, $showStamp, $instance_id)
    {
        $counter = 0;
        $erraseShow = false;
        if (count($currentShowStamp) != count($showStamp)) {
            $erraseShow = true;
        } else {
            foreach ($showStamp as $item) {
                if ($item["file_id"] != $currentShowStamp[$counter]["file_id"] ||
                    $item["stream_id"] != $currentShowStamp[$counter]["stream_id"]) {
                        $erraseShow = true;
                        break;
                    //CcScheduleQuery::create()
                    //    ->filterByDbInstanceId($ccShowInstance->getDbId())
                    //    ->delete();
                 }
                 $counter += 1;
            }
        }
        if ($erraseShow) {
            $delete_sql = "DELETE FROM cc_schedule ".
                    "WHERE instance_id = :instance_id";
            Application_Common_Database::prepareAndExecute(
            $delete_sql, array(), Application_Common_Database::EXECUTE);
            return true;
        }

        //If we get here, the content in the show instance is the same
        // as what we want to replace it with, so we can leave as is

        return false;
    }*/

    public function emptyShowContent($instanceId)
    {
        try {
            $ccShowInstance = CcShowInstancesQuery::create()->findPk($instanceId);

            $instances = [];
            $instanceIds = [];

            if ($ccShowInstance->getCcShow()->isLinked()) {
                foreach ($ccShowInstance->getCcShow()->getFutureCcShowInstancess() as $instance) {
                    $instanceIds[] = $instance->getDbId();
                    $instances[] = $instance;
                }
            } else {
                $instanceIds[] = $ccShowInstance->getDbId();
                $instances[] = $ccShowInstance;
            }

            /* Get the file ids of the tracks we are about to delete
             * from cc_schedule. We need these so we can update the
             * is_scheduled flag in cc_files
             */
            $ccSchedules = CcScheduleQuery::create()
                ->filterByDbInstanceId($instanceIds, Criteria::IN)
                ->setDistinct(CcSchedulePeer::FILE_ID)
                ->find();
            $fileIds = [];
            foreach ($ccSchedules as $ccSchedule) {
                $fileIds[] = $ccSchedule->getDbFileId();
            }

            // Clear out the schedule
            CcScheduleQuery::create()
                ->filterByDbInstanceId($instanceIds, Criteria::IN)
                ->delete();

            /* Now that the schedule has been cleared we need to make
             * sure we do not update the is_scheduled flag for tracks
             * that are scheduled in other shows
             */
            $futureScheduledFiles = Application_Model_Schedule::getAllFutureScheduledFiles();
            foreach ($fileIds as $k => $v) {
                if (in_array($v, $futureScheduledFiles)) {
                    unset($fileIds[$k]);
                }
            }

            $selectCriteria = new Criteria();
            $selectCriteria->add(CcFilesPeer::ID, $fileIds, Criteria::IN);
            $updateCriteria = new Criteria();
            $updateCriteria->add(CcFilesPeer::IS_SCHEDULED, false);
            BasePeer::doUpdate($selectCriteria, $updateCriteria, Propel::getConnection());

            Application_Model_RabbitMq::PushSchedule();
            $con = Propel::getConnection(CcShowInstancesPeer::DATABASE_NAME);
            foreach ($instances as $instance) {
                $instance->updateDbTimeFilled($con);
            }

            return true;
        } catch (Exception $e) {
            Logging::info($e->getMessage());

            return false;
        }
    }

    // TODO in the future this should probably support webstreams.
    public function updateFutureIsScheduled($scheduleId, $status)
    {
        $sched = CcScheduleQuery::create()->findPk($scheduleId);
        $redraw = false;

        if (isset($sched)) {
            $fileId = $sched->getDbFileId();

            if (isset($fileId)) {
                $redraw = Application_Model_StoredFile::setIsScheduled($fileId, $status);
            }
        }

        return $redraw;
    }
}