<?php

/**
 *  Application_Model_StoredFile class.
 *
 * @copyright 2010 Sourcefabric O.P.S.
 * @license https://www.gnu.org/licenses/gpl.txt
 *
 * @see MetaData
 */
class Application_Model_StoredFile
{
    /**
     * @holds propel database object
     *
     * @var CcFiles
     */
    private $_file;

    /**
     * @holds PDO object reference
     */
    private $_con;

    /**
     * array of db metadata -> propel.
     */
    private $_dbMD = [
        'track_title' => 'DbTrackTitle',
        'artist_name' => 'DbArtistName',
        'album_title' => 'DbAlbumTitle',
        'genre' => 'DbGenre',
        'mood' => 'DbMood',
        'track_number' => 'DbTrackNumber',
        'bpm' => 'DbBpm',
        'label' => 'DbLabel',
        'composer' => 'DbComposer',
        'encoded_by' => 'DbEncodedBy',
        'conductor' => 'DbConductor',
        'year' => 'DbYear',
        'info_url' => 'DbInfoUrl',
        'isrc_number' => 'DbIsrcNumber',
        'copyright' => 'DbCopyright',
        'length' => 'DbLength',
        'bit_rate' => 'DbBitRate',
        'sample_rate' => 'DbSampleRate',
        'mime' => 'DbMime',
        // "md5"          => "DbMd5",
        'ftype' => 'DbFtype',
        'language' => 'DbLanguage',
        'replay_gain' => 'DbReplayGain',
        'owner_id' => 'DbOwnerId',
        'cuein' => 'DbCueIn',
        'cueout' => 'DbCueOut',
        'description' => 'DbDescription',
        'artwork' => 'DbArtwork',
        'track_type_id' => 'DbTrackTypeId',
    ];

    public function __construct($file, $con)
    {
        $this->_file = $file;
        $this->_con = $con;
    }

    public function getId()
    {
        return $this->_file->getDbId();
    }

    public function getFormat()
    {
        return $this->_file->getDbFtype();
    }

    /**
     * @return CcFiles
     */
    public function getPropelOrm()
    {
        return $this->_file;
    }

    public function setFormat($p_format)
    {
        $this->_file->setDbFtype($p_format);
    }

    /* This function is only called after liquidsoap
     * has notified that a track has started playing.
     */
    public function setLastPlayedTime($p_now)
    {
        $this->_file->setDbLPtime($p_now);
        /* Normally we would only call save after all columns have been set
         * like in setDbColMetadata(). But since we are only setting one
         * column in this case it is OK.
         */
        $this->_file->save();
    }

    public static function createWithFile($f, $con)
    {
        return new Application_Model_StoredFile($f, $con);
    }

    /**
     * Set multiple metadata values using defined metadata constants.
     *
     * @param array $p_md
     *                    example: $p_md['MDATA_KEY_URL'] = 'https://example.org'
     */
    public function setMetadata($p_md = null)
    {
        if (is_null($p_md)) {
            $this->setDbColMetadata();
        } else {
            $dbMd = [];

            if (isset($p_md['MDATA_KEY_YEAR'])) {
                // We need to make sure to clean this value before
                // inserting into database. If value is outside of range
                // [-2^31, 2^31-1] then postgresl will throw error when
                // trying to retrieve this value. We could make sure
                // number is within these bounds, but simplest is to do
                // substring to 4 digits (both values are garbage, but
                // at least our new garbage value won't cause errors).
                // If the value is 2012-01-01, then substring to first 4
                // digits is an OK result. CC-3771

                $year = $p_md['MDATA_KEY_YEAR'];

                if (strlen($year) > 4) {
                    $year = substr($year, 0, 4);
                }
                if (!is_numeric($year)) {
                    $year = 0;
                }
                $p_md['MDATA_KEY_YEAR'] = $year;
            }

            // Translate metadata attributes from media monitor (MDATA_KEY_*)
            // to their counterparts in constants.php (usually the column names)
            $track_length = $p_md['MDATA_KEY_DURATION'];
            $track_length_in_sec = Application_Common_DateHelper::calculateLengthInSeconds($track_length);
            foreach ($p_md as $mdConst => $mdValue) {
                if (defined($mdConst)) {
                    if ($mdConst == 'MDATA_KEY_CUE_OUT') {
                        if ($mdValue == '0.0') {
                            $mdValue = $track_length_in_sec;
                        } else {
                            $this->_file->setDbSilanCheck(true)->save();
                        }
                    }
                    $dbMd[constant($mdConst)] = $mdValue;
                }
            }
            $this->setDbColMetadata($dbMd);
        }
    }

    /**
     * Set multiple metadata values using database columns as indexes.
     *
     * @param array $p_md
     *                    example: $p_md['url'] = 'https://www.example.com'
     */
    public function setDbColMetadata($p_md = null)
    {
        if (is_null($p_md)) {
            foreach ($this->_dbMD as $dbColumn => $propelColumn) {
                $method = "set{$propelColumn}";
                $this->_file->{$method}(null);
            }
        } else {
            // in order to edit the owner of a file we see if owner_id exists in the track form metadata otherwise
            // we determine it via the algorithm below
            if (!array_key_exists('owner_id', $p_md)) {
                $owner = $this->_file->getFkOwner();
                // if owner_id is already set we don't want to set it again.
                if (!$owner) { // no owner detected, we try to assign one.
                    // if MDATA_OWNER_ID is not set then we default to the
                    // first admin user we find
                    if (!array_key_exists('owner_id', $p_md)) {
                        // $admins = Application_Model_User::getUsers(array('A'));
                        $admins = array_merge(
                            Application_Model_User::getUsersOfType('A')->getData(),
                            Application_Model_User::getUsersOfType('S')->getData()
                        );
                        if (count($admins) > 0) { // found admin => pick first one
                            $owner = $admins[0];
                        }
                    } // get the user by id and set it like that
                    else {
                        $user = CcSubjsQuery::create()
                            ->findPk($p_md['owner_id']);
                        if ($user) {
                            $owner = $user;
                        }
                    }
                    if ($owner) {
                        $this->_file->setDbOwnerId($owner->getDbId());
                    } else {
                        Logging::info("Could not find suitable owner for file
                        '" . $p_md['filepath'] . "'");
                    }
                }
            }
            foreach ($p_md as $dbColumn => $mdValue) {
                // don't blank out name, defaults to original filename on first
                // insertion to database.
                if ($dbColumn == 'track_title' && (is_null($mdValue) || $mdValue == '')) {
                    continue;
                }

                // Bpm gets POSTed as a string type. With Propel 1.6 this value
                // was casted to an integer type before saving it to the db. But
                // Propel 1.7 does not do this
                if ($dbColumn == 'bpm') {
                    $mdValue = (int) $mdValue;
                }
                // TODO : refactor string evals
                if (isset($this->_dbMD[$dbColumn])) {
                    $propelColumn = $this->_dbMD[$dbColumn];
                    $method = "set{$propelColumn}";

                    /* We need to set track_number to null if it is an empty string
                     * because propel defaults empty strings to zeros */
                    if ($dbColumn == 'track_number' && empty($mdValue)) {
                        $mdValue = null;
                    }

                    // We need to set track_type_id to null if it is an empty string or 0
                    if ($dbColumn == 'track_type_id' && ($mdValue == 0 || empty($mdValue))) {
                        $mdValue = null;
                    }

                    $this->_file->{$method}($mdValue);
                }
            }
        }

        $this->_file->setDbMtime(new DateTime('now', new DateTimeZone('UTC')));
        $this->_file->save($this->_con);
    }

    /**
     * Set metadata element value.
     *
     * @param mixed $p_category
     * @param mixed $p_value
     */
    public function setMetadataValue($p_category, $p_value)
    {
        // constant() was used because it gets quoted constant name value from
        // api_client.py. This is the wrapper funtion
        $this->setDbColMetadataValue(constant($p_category), $p_value);
    }

    /**
     * Set metadata element value.
     *
     * @param mixed $p_category
     * @param mixed $p_value
     */
    public function setDbColMetadataValue($p_category, $p_value)
    {
        // don't blank out name, defaults to original filename on first insertion to database.
        if ($p_category == 'track_title' && (is_null($p_value) || $p_value == '')) {
            return;
        }
        if (isset($this->_dbMD[$p_category])) {
            // TODO : fix this crust -- RG
            $propelColumn = $this->_dbMD[$p_category];
            $method = "set{$propelColumn}";
            $this->_file->{$method}($p_value);
            $this->_file->save();
        }
    }

    /**
     * Get metadata as array, indexed by the column names in the database.
     *
     * @return array
     */
    public function getDbColMetadata()
    {
        $md = [];
        foreach ($this->_dbMD as $dbColumn => $propelColumn) {
            $method = "get{$propelColumn}";
            $md[$dbColumn] = $this->_file->{$method}();
        }

        return $md;
    }

    /**
     * Get metadata as array, indexed by the constant names.
     *
     * @return array
     */
    public function getMetadata()
    {
        $c = get_defined_constants(true);
        $md = [];

        /* Create a copy of dbMD here and create a "filepath" key inside of
         * it. The reason we do this here, instead of creating this key inside
         * dbMD is because "filepath" isn't really metadata, and we don't want
         * filepath updated everytime the metadata changes. Also it needs extra
         * processing before we can write it to the database (needs to be split
         * into base and relative path)
         *  */
        $dbmd_copy = $this->_dbMD;
        $dbmd_copy['filepath'] = 'DbFilepath';

        foreach ($c['user'] as $constant => $value) {
            if (preg_match('/^MDATA_KEY/', $constant)) {
                if (isset($dbmd_copy[$value])) {
                    $propelColumn = $dbmd_copy[$value];
                    $method = "get{$propelColumn}";
                    $md[$constant] = $this->_file->{$method}();
                }
            }
        }

        return $md;
    }

    /**
     * Returns an array of playlist objects that this file is a part of.
     *
     * @return array
     */
    public function getPlaylists()
    {
        $con = Propel::getConnection();

        $sql = <<<'SQL'
SELECT playlist_id
FROM cc_playlist
WHERE file_id = :file_id
SQL;

        $stmt = $con->prepare($sql);
        $stmt->bindParam(':file_id', $this->id, PDO::PARAM_INT);

        if ($stmt->execute()) {
            $ids = $stmt->fetchAll();
        } else {
            $msg = implode(',', $stmt->errorInfo());

            throw new Exception("Error: {$msg}");
        }

        if (is_array($ids) && count($ids) > 0) {
            return array_map(function ($id) {
                return Application_Model_Playlist::RecallById($id);
            }, $ids);
        } else {
            return [];
        }
    }

    /**
     * Check if the file (on disk) corresponding to this class exists or not.
     *
     * @return bool true if the file exists, false otherwise
     */
    public function existsOnDisk()
    {
        $exists = false;

        try {
            $filePaths = $this->getFilePaths();
            $filePath = $filePaths[0];
            $exists = (file_exists($filePath) && !is_dir($filePath));
        } catch (Exception $e) {
            return false;
        }

        return $exists;
    }

    /**
     * Deletes the physical file from the local file system.
     *
     * @param mixed $quiet
     */
    public function delete($quiet = false)
    {
        // Check if the file is scheduled to be played in the future
        if (Application_Model_Schedule::IsFileScheduledInTheFuture($this->_file->getCcFileId())) {
            throw new DeleteScheduledFileException();
        }

        // if we get here from the REST API, there's no valid user. APIKEY is validated already.
        if ($userInfo = Zend_Auth::getInstance()->getStorage()->read()) {
            // This call will throw "Trying to get property 'id' of non-object"
            $user = new Application_Model_User($userInfo->id);
            $isAdminOrPM = $user->isUserType([UTYPE_SUPERADMIN, UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER]);
            if (!$isAdminOrPM && $this->getFileOwnerId() != $user->getId()) {
                throw new FileNoPermissionException();
            }
            $file_id = $this->_file->getDbId();
            Logging::info($file_id);
            Logging::info('User ' . $user->getLogin() . ' is deleting file: ' . $this->_file->getDbTrackTitle() . ' - file id: ' . $file_id);
        } else {
            Logging::info('API Auth is deleting file: ' . $this->_file->getDbTrackTitle() . ' - file id: ' . $this->_file->getDbId());
        }
        $filesize = $this->_file->getFileSize();
        if ($filesize < 0) {
            throw new Exception('Cannot delete file with filesize ' . $filesize);
        }

        // Delete the physical file from either the local stor directory
        if ($this->_file->getDbImportStatus() == CcFiles::IMPORT_STATUS_SUCCESS) {
            try {
                $this->_file->deletePhysicalFile();
            } catch (Exception $e) {
                if ($quiet) {
                    Logging::info($e);
                } else {
                    // Just log the exception and continue.
                    Logging::error($e);
                }
            }
        }

        // Update the user's disk usage
        Application_Model_Preference::updateDiskUsage(-1 * $filesize);

        // Explicitly update any playlist's and block's length that contain
        // the file getting deleted
        self::updateBlockAndPlaylistLength($this->_file->getDbId());

        // delete the file record from cc_files
        $this->_file->delete();
    }

    /*
     * This function is meant to be called when a file is getting
     * deleted from the library. It re-calculates the length of
     * all blocks and playlists that contained the deleted file.
     */
    private static function updateBlockAndPlaylistLength($fileId)
    {
        $plRows = CcPlaylistcontentsQuery::create()->filterByDbFileId($fileId)->find();
        foreach ($plRows as $row) {
            $pl = CcPlaylistQuery::create()->filterByDbId($row->getDbPlaylistId($fileId))->findOne();
            $pl->setDbLength($pl->computeDbLength(Propel::getConnection(CcPlaylistPeer::DATABASE_NAME)));
            $pl->save();
        }

        $blRows = CcBlockcontentsQuery::create()->filterByDbFileId($fileId)->find();
        foreach ($blRows as $row) {
            $bl = CcBlockQuery::create()->filterByDbId($row->getDbBlockId())->findOne();
            $bl->setDbLength($bl->computeDbLength(Propel::getConnection(CcBlockPeer::DATABASE_NAME)));
            $bl->save();
        }
    }

    /**
     * This function is for when media monitor detects deletion of file
     * and trying to update airtime side.
     *
     * @param mixed $deleteFromPlaylist
     */
    public function deleteByMediaMonitor($deleteFromPlaylist = false)
    {
        if ($deleteFromPlaylist) {
            Application_Model_Playlist::DeleteFileFromAllPlaylists($this->getId());
        }
        // set file_exists flag to false
        $this->_file->setDbFileExists(false);
        $this->_file->save();
    }

    /**
     * Get the absolute filepath.
     *
     * @return array of strings
     */
    public function getFilePaths()
    {
        assert($this->_file);

        return $this->_file->getURLsForTrackPreviewOrDownload();
    }

    /**
     * Set real filename of raw media data.
     *
     * @param mixed $p_filepath
     *
     * @return string
     */
    public function setFilePath($p_filepath)
    {
        $path_info = Application_Common_Storage::splitFilePath($p_filepath);

        if (is_null($path_info)) {
            return -1;
        }
        $this->_file->setDbFilepath($path_info[1]);
        $this->_file->save($this->_con);
    }

    /**
     * Get the URL to access this file.
     */
    public function getFileUrl()
    {
        return $this->getRelativeFileUrl(Config::getPublicUrl());
    }

    /**
     * Sometimes we want a relative URL and not a full URL. See bug
     * https://dev.sourcefabric.org/browse/CC-2403.
     *
     * @param mixed $baseUrl
     */
    public function getRelativeFileUrl($baseUrl)
    {
        return $baseUrl . 'api/get-media/file/' . $this->getId();
    }

    public function getResourceId()
    {
        return $this->_file->getResourceId();
    }

    public function getFileSize()
    {
        $filesize = $this->_file->getFileSize();

        // It's OK for the file size to be zero. Pypo will make a request to Airtime and update
        // the file size and md5 hash if they are not set.
        if ($filesize < 0) {
            throw new Exception('Could not determine filesize for file id: ' . $this->_file->getDbId() . '. Filesize: ' . $filesize);
        }

        return $filesize;
    }

    public static function Insert($md, $con)
    {
        // save some work by checking if filepath is given right away
        if (!isset($md['MDATA_KEY_FILEPATH'])) {
            return null;
        }

        $file = new CcFiles();
        $now = new DateTime('now', new DateTimeZone('UTC'));
        $file->setDbUtime($now);
        $file->setDbMtime($now);

        $storedFile = new Application_Model_StoredFile($file, $con);

        // removed "//" in the path. Always use '/' for path separator
        // TODO : it might be better to just call OsPath::normpath on the file
        // path. Also note that mediamonitor normalizes the paths anyway
        // before passing them to php so it's not necessary to do this at all

        $filepath = str_replace('//', '/', $md['MDATA_KEY_FILEPATH']);
        $res = $storedFile->setFilePath($filepath);
        if ($res === -1) {
            return null;
        }
        $storedFile->setMetadata($md);

        return $storedFile;
    }

    /* TODO: Callers of this function should use a Propel transaction. Start
     * by creating $con outside the function with beingTransaction() */
    /**
     * @param int $p_id
     * @param Propel Connection
     * @param null|mixed $con
     *
     * @return Application_Model_StoredFile
     *
     * @throws Exception
     */
    public static function RecallById($p_id = null, $con = null)
    {
        // TODO
        if (is_null($con)) {
            $con = Propel::getConnection(CcFilesPeer::DATABASE_NAME);
        }

        if (isset($p_id)) {
            $p_id = intval($p_id);

            $storedFile = CcFilesQuery::create()->findPK($p_id, $con);
            if (is_null($storedFile)) {
                throw new Exception('Could not recall file with id: ' . $p_id);
            }

            return self::createWithFile($storedFile, $con);
        }

        throw new Exception('No arguments passed to RecallById');
    }

    public function getName()
    {
        return $this->_file->getFilename();
    }

    /**
     * Fetch the Application_Model_StoredFile by looking up its filepath.
     *
     * @param string $p_filepath path of file stored in Airtime
     * @param mixed  $con
     *
     * @return null|Application_Model_StoredFile
     */
    public static function RecallByFilepath($p_filepath, $con)
    {
        $path_info = Application_Common_Storage::splitFilePath($p_filepath);

        if (is_null($path_info)) {
            return null;
        }

        $file = CcFilesQuery::create()
            ->filterByDbFilepath($path_info[1])
            ->findOne($con);

        return is_null($file) ? null : self::createWithFile($file, $con);
    }

    public static function RecallByPartialFilepath($partial_path, $con)
    {
        $path_info = Application_Common_Storage::splitFilePath($partial_path);

        if (is_null($path_info)) {
            return null;
        }

        $files = CcFilesQuery::create()
            ->filterByDbFilepath("{$path_info[1]}%")
            ->find($con);
        $res = [];
        foreach ($files as $file) {
            $storedFile = new Application_Model_StoredFile($file, $con);
            $res[] = $storedFile;
        }

        return $res;
    }

    public static function getLibraryColumns()
    {
        return [
            'id', 'track_title', 'artist_name', 'album_title',
            'genre', 'length', 'year', 'utime', 'mtime', 'ftype',
            'track_number', 'mood', 'bpm', 'composer', 'info_url',
            'bit_rate', 'sample_rate', 'isrc_number', 'encoded_by', 'label',
            'copyright', 'mime', 'language', 'filepath', 'owner_id',
            'conductor', 'replay_gain', 'lptime', 'is_playlist', 'is_scheduled',
            'cuein', 'cueout', 'description', 'artwork', 'track_type_id',
        ];
    }

    public static function searchLibraryFiles($datatables)
    {
        $con = Propel::getConnection(CcFilesPeer::DATABASE_NAME);

        $displayColumns = self::getLibraryColumns();

        $plSelect = [];
        $blSelect = [];
        $fileSelect = [];
        $streamSelect = [];
        foreach ($displayColumns as $key) {
            if ($key === 'id') {
                $plSelect[] = 'PL.id AS ' . $key;
                $blSelect[] = 'BL.id AS ' . $key;
                $fileSelect[] = "FILES.id AS {$key}";
                $streamSelect[] = 'ws.id AS ' . $key;
            } elseif ($key === 'track_title') {
                $plSelect[] = 'name AS ' . $key;
                $blSelect[] = 'name AS ' . $key;
                $fileSelect[] = $key;
                $streamSelect[] = 'name AS ' . $key;
            } elseif ($key === 'ftype') {
                $plSelect[] = "'playlist'::varchar AS " . $key;
                $blSelect[] = "'block'::varchar AS " . $key;
                $fileSelect[] = $key;
                $streamSelect[] = "'stream'::varchar AS " . $key;
            } elseif ($key === 'artist_name') {
                $plSelect[] = 'login AS ' . $key;
                $blSelect[] = 'login AS ' . $key;
                $fileSelect[] = $key;
                $streamSelect[] = 'login AS ' . $key;
            } elseif ($key === 'owner_id') {
                $plSelect[] = 'login AS ' . $key;
                $blSelect[] = 'login AS ' . $key;
                $fileSelect[] = "sub.login AS {$key}";
                $streamSelect[] = 'login AS ' . $key;
            } elseif ($key === 'replay_gain') {
                $plSelect[] = 'NULL::NUMERIC AS ' . $key;
                $blSelect[] = 'NULL::NUMERIC AS ' . $key;
                $fileSelect[] = $key;
                $streamSelect[] = 'NULL::NUMERIC AS ' . $key;
            } elseif ($key === 'lptime') {
                $plSelect[] = 'NULL::TIMESTAMP AS ' . $key;
                $blSelect[] = 'NULL::TIMESTAMP AS ' . $key;
                $fileSelect[] = $key;
                $streamSelect[] = $key;
            } elseif ($key === 'is_scheduled' || $key === 'is_playlist') {
                $plSelect[] = 'NULL::boolean AS ' . $key;
                $blSelect[] = 'NULL::boolean AS ' . $key;
                $fileSelect[] = $key;
                $streamSelect[] = 'NULL::boolean AS ' . $key;
            } elseif ($key === 'cuein' || $key === 'cueout') {
                $plSelect[] = 'NULL::INTERVAL AS ' . $key;
                $blSelect[] = 'NULL::INTERVAL AS ' . $key;
                $fileSelect[] = $key;
                $streamSelect[] = 'NULL::INTERVAL AS ' . $key;
            }
            // file length is displayed based on cueout - cuein.
            elseif ($key === 'length') {
                $plSelect[] = $key;
                $blSelect[] = $key;
                $fileSelect[] = '(cueout - cuein)::INTERVAL AS length';
                $streamSelect[] = $key;
            }
            // same columns in each table.
            elseif (in_array($key, ['utime', 'mtime'])) {
                $plSelect[] = $key;
                $blSelect[] = $key;
                $fileSelect[] = $key;
                $streamSelect[] = $key;
            } elseif ($key === 'year') {
                $plSelect[] = 'EXTRACT(YEAR FROM utime)::varchar AS ' . $key;
                $blSelect[] = 'EXTRACT(YEAR FROM utime)::varchar AS ' . $key;
                $fileSelect[] = 'year AS ' . $key;
                $streamSelect[] = 'EXTRACT(YEAR FROM utime)::varchar AS ' . $key;
            }
            // need to cast certain data as ints for the union to search on.
            elseif (in_array($key, ['track_number', 'bit_rate', 'sample_rate', 'bpm', 'track_type_id'])) {
                $plSelect[] = 'NULL::int AS ' . $key;
                $blSelect[] = 'NULL::int AS ' . $key;
                $fileSelect[] = $key;
                $streamSelect[] = 'NULL::int AS ' . $key;
            } elseif ($key === 'filepath') {
                $plSelect[] = 'NULL::VARCHAR AS ' . $key;
                $blSelect[] = 'NULL::VARCHAR AS ' . $key;
                $fileSelect[] = $key;
                $streamSelect[] = 'url AS ' . $key;
            } elseif ($key == 'mime') {
                $plSelect[] = 'NULL::VARCHAR AS ' . $key;
                $blSelect[] = 'NULL::VARCHAR AS ' . $key;
                $fileSelect[] = $key;
                $streamSelect[] = $key;
            } else {
                $plSelect[] = 'NULL::text AS ' . $key;
                $blSelect[] = 'NULL::text AS ' . $key;
                $fileSelect[] = $key;
                $streamSelect[] = 'NULL::text AS ' . $key;
            }
        }

        $plSelect = 'SELECT ' . implode(',', $plSelect);
        $blSelect = 'SELECT ' . implode(',', $blSelect);
        $fileSelect = 'SELECT ' . implode(',', $fileSelect);
        $streamSelect = 'SELECT ' . implode(',', $streamSelect);

        $type = intval($datatables['type']);

        $plTable = "({$plSelect} FROM cc_playlist AS PL LEFT JOIN cc_subjs AS sub ON (sub.id = PL.creator_id))";
        $blTable = "({$blSelect} FROM cc_block AS BL LEFT JOIN cc_subjs AS sub ON (sub.id = BL.creator_id))";
        $fileTable = "({$fileSelect} FROM cc_files AS FILES LEFT JOIN cc_subjs AS sub ON (sub.id = FILES.owner_id) WHERE file_exists = 'TRUE' AND hidden='FALSE')";
        // $fileTable = "({$fileSelect} FROM cc_files AS FILES WHERE file_exists = 'TRUE')";
        $streamTable = "({$streamSelect} FROM cc_webstream AS ws LEFT JOIN cc_subjs AS sub ON (sub.id = ws.creator_id))";
        $unionTable = "({$plTable} UNION {$blTable} UNION {$fileTable} UNION {$streamTable}) AS RESULTS";

        // choose which table we need to select data from.
        switch ($type) {
            case MediaType::FILE:
                $fromTable = $fileTable . ' AS File'; // need an alias for the table if it's standalone.

                break;

            case MediaType::PLAYLIST:
                $fromTable = $plTable . ' AS Playlist'; // need an alias for the table if it's standalone.

                break;

            case MediaType::BLOCK:
                $fromTable = $blTable . ' AS Block'; // need an alias for the table if it's standalone.

                break;

            case MediaType::WEBSTREAM:
                $fromTable = $streamTable . ' AS StreamTable'; // need an alias for the table if it's standalone.

                break;

            default:
                $fromTable = $unionTable;
        }

        // update is_scheduled to false for tracks that
        // have already played out
        self::updatePastFilesIsScheduled();
        $results = Application_Model_Datatables::findEntries($con, $displayColumns, $fromTable, $datatables);

        $displayTimezone = new DateTimeZone(Application_Model_Preference::GetUserTimezone());
        $utcTimezone = new DateTimeZone('UTC');

        $fp = Config::getStoragePath();

        foreach ($results['aaData'] as &$row) {
            $row['id'] = intval($row['id']);

            // taken from Datatables.php, needs to be cleaned up there.
            if (isset($r['ftype'])) {
                if ($r['ftype'] == 'playlist') {
                    $pl = new Application_Model_Playlist($r['id']);
                    $r['length'] = $pl->getLength();
                } elseif ($r['ftype'] == 'block') {
                    $bl = new Application_Model_Block($r['id']);
                    $r['bl_type'] = $bl->isStatic() ? 'static' : 'dynamic';
                    $r['length'] = $bl->getLength();
                }
            }

            if ($row['ftype'] === 'audioclip') {
                $cuein_formatter = new LengthFormatter($row['cuein']);
                $row['cuein'] = $cuein_formatter->format();

                $cueout_formatter = new LengthFormatter($row['cueout']);
                $row['cueout'] = $cueout_formatter->format();

                $cuein = Application_Common_DateHelper::playlistTimeToSeconds($row['cuein']);
                $cueout = Application_Common_DateHelper::playlistTimeToSeconds($row['cueout']);
                $row_length = Application_Common_DateHelper::secondsToPlaylistTime($cueout - $cuein);

                $formatter = new SamplerateFormatter($row['sample_rate']);
                $row['sample_rate'] = $formatter->format();

                $formatter = new BitrateFormatter($row['bit_rate']);
                $row['bit_rate'] = $formatter->format();

                $get_artwork = FileDataHelper::getArtworkData($row['artwork'], 32, $fp);
                $row['artwork_data'] = $get_artwork;

                // for audio preview
                $row['audioFile'] = $row['id'] . '.' . pathinfo($row['filepath'], PATHINFO_EXTENSION);
            } else {
                $row['audioFile'] = $row['id'];
                $row_length = $row['length'];
            }

            $len_formatter = new LengthFormatter($row_length);
            $row['length'] = $len_formatter->format();

            // convert mtime and utime to localtime
            $row['mtime'] = new DateTime($row['mtime'], $utcTimezone);
            $row['mtime']->setTimeZone($displayTimezone);
            $row['mtime'] = $row['mtime']->format(DEFAULT_TIMESTAMP_FORMAT);
            $row['utime'] = new DateTime($row['utime'], $utcTimezone);
            $row['utime']->setTimeZone($displayTimezone);
            $row['utime'] = $row['utime']->format(DEFAULT_TIMESTAMP_FORMAT);

            // need to convert last played to localtime if it exists.
            if (isset($row['lptime'])) {
                $row['lptime'] = new DateTime($row['lptime'], $utcTimezone);
                $row['lptime']->setTimeZone($displayTimezone);
                $row['lptime'] = $row['lptime']->format(DEFAULT_TIMESTAMP_FORMAT);
            }

            // we need to initalize the checkbox and image row because we do not retrieve
            // any data from the db for these and datatables will complain
            $row['checkbox'] = '';
            $row['image'] = '';
            $row['options'] = '';

            $type = substr($row['ftype'], 0, 2);
            $row['tr_id'] = "{$type}_{$row['id']}";
        }

        return $results;
    }

    /**
     * Copy a newly uploaded audio file from its temporary upload directory
     * on the local disk (like /tmp) over to Airtime's "stor" directory,
     * which is where all ingested music/media live.
     *
     * This is done in PHP here on the web server rather than in libretime-analyzer because
     * the libretime-analyzer might be running on a different physical computer than the web server,
     * and it probably won't have access to the web server's /tmp folder. The stor/organize directory
     * is, however, both accessible to the machines running libretime-analyzer and the web server
     * on Airtime Pro.
     *
     * The file is actually copied to "stor/organize", which is a staging directory where files go
     * before they're processed by libretime-analyzer, which then moves them to "stor/imported" in the final
     * step.
     *
     * @param string $tempFilePath
     * @param string $originalFilename
     * @param bool   $copyFile         copy the file instead of moving it
     * @param mixed  $fileId
     *
     * @return Ambigous <unknown, string>
     *
     * @throws Exception
     */
    public static function moveFileToStor($tempFilePath, $fileId, $originalFilename, $copyFile = false)
    {
        $audio_file = $tempFilePath;

        $stor = Config::getStoragePath();
        // check if "organize" dir exists and if not create one
        if (!file_exists($stor . '/organize')) {
            if (!mkdir($stor . '/organize', 0777)) {
                throw new Exception('Failed to create organize directory.');
            }
        }

        if (chmod($audio_file, 0644) === false) {
            Logging::info("Warning: couldn't change permissions of {$audio_file} to 0644");
        }

        // Did all the checks for real, now trying to copy
        $audio_stor = Application_Common_OsPath::join(
            $stor,
            'organize',
            $fileId . '-' . $originalFilename
        );
        // if the uploaded file is not UTF-8 encoded, let's encode it. Assuming source
        // encoding is ISO-8859-1
        $audio_stor = mb_detect_encoding($audio_stor, 'UTF-8') == 'UTF-8' ? $audio_stor : utf8_encode($audio_stor);
        if ($copyFile) {
            Logging::info("Copying file {$audio_file} to {$audio_stor}");
            if (@copy($audio_file, $audio_stor) === false) {
                throw new Exception("Failed to copy {$audio_file} to {$audio_stor}");
            }
        } else {
            Logging::info("Moving file {$audio_file} to {$audio_stor}");

            // Ensure we have permissions to overwrite the file in stor, in case it already exists.
            if (file_exists($audio_stor)) {
                chmod($audio_stor, 0644);
            }

            // Martin K.: changed to rename: Much less load + quicker since this is
            // an atomic operation
            if (rename($audio_file, $audio_stor) === false) {
                // something went wrong likely there wasn't enough space in .
                // the audio_stor to move the file too warn the user that   .
                // the file wasn't uploaded and they should check if there  .
                // is enough disk space                                     .
                unlink($audio_file); // remove the file after failed rename

                throw new Exception('The file was not uploaded, this error can occur if the computer '
                    . 'hard drive does not have enough disk space or the stor '
                    . 'directory does not have correct write permissions.');
            }
        }

        return $audio_stor;
    }

    // Pass the file through Liquidsoap and test if it is readable. Return True if readable, and False otherwise.
    public static function liquidsoapFilePlayabilityTest($audio_file)
    {
        $LIQUIDSOAP_ERRORS = ['TagLib: MPEG::Properties::read() -- Could not find a valid last MPEG frame in the stream.'];

        // Ask Liquidsoap if file is playable
        // CC-5990/5991 - Changed to point directly to liquidsoap, removed PATH export
        $command = sprintf(
            'liquidsoap -v -c "output.dummy(audio_to_stereo(single(%s)))" 2>&1',
            escapeshellarg($audio_file)
        );

        exec($command, $output, $rv);

        $isError = count($output) > 0 && in_array($output[0], $LIQUIDSOAP_ERRORS);

        Logging::info('Is error?! : ' . $isError);
        Logging::info('ls playability response: ' . $rv);

        return $rv == 0 && !$isError;
    }

    public static function getFileCount()
    {
        $sql = 'SELECT count(*) as cnt FROM cc_files WHERE file_exists';

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

    /**
     * Enter description here ...
     *
     * @param       $dir_id - if this is not provided, it returns all files with full
     *                     path constructed
     * @param mixed $all
     */
    public static function listAllFiles($dir_id = null, $all = true)
    {
        $con = Propel::getConnection();

        $sql = <<<'SQL'
SELECT filepath AS fp
FROM CC_FILES AS f
WHERE f.directory = :dir_id
SQL;

        if (!$all) {
            $sql .= " AND f.file_exists = 'TRUE'";
        }

        $stmt = $con->prepare($sql);
        $stmt->bindParam(':dir_id', $dir_id);

        if ($stmt->execute()) {
            $rows = $stmt->fetchAll();
        } else {
            $msg = implode(',', $stmt->errorInfo());

            throw new Exception("Error: {$msg}");
        }

        $results = [];
        foreach ($rows as $row) {
            $results[] = $row['fp'];
        }

        return $results;
    }

    // TODO: MERGE THIS FUNCTION AND "listAllFiles" -MK
    public static function listAllFiles2($dir_id = null, $limit = 'ALL')
    {
        $con = Propel::getConnection();

        $sql = <<<'SQL'
SELECT id,
       filepath AS fp
FROM cc_files
WHERE directory = :dir_id
  AND file_exists = 'TRUE'
  AND replay_gain IS NULL LIMIT :lim
SQL;

        $stmt = $con->prepare($sql);
        $stmt->bindParam(':dir_id', $dir_id);
        $stmt->bindParam(':lim', $limit);

        if ($stmt->execute()) {
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        } else {
            $msg = implode(',', $stmt->errorInfo());

            throw new Exception("Error: {$msg}");
        }

        return $rows;
    }

    public static function getAllFilesWithoutSilan()
    {
        $con = Propel::getConnection();

        $sql = <<<'SQL'
SELECT f.id,
       m.directory || f.filepath AS fp
FROM cc_files as f
JOIN cc_music_dirs as m ON f.directory = m.id
WHERE file_exists = 'TRUE'
  AND silan_check IS FALSE Limit 100
SQL;
        $stmt = $con->prepare($sql);

        if ($stmt->execute()) {
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        } else {
            $msg = implode(',', $stmt->errorInfo());

            throw new Exception("Error: {$msg}");
        }

        return $rows;
    }

    public function setFileExistsFlag($flag)
    {
        $this->_file->setDbFileExists($flag)
            ->save();
    }

    public function setFileHiddenFlag($flag)
    {
        $this->_file->setDbHidden($flag)
            ->save();
    }

    // This method seems to be unsued everywhere so I've commented it out
    // If it's absence does not have any effect then it will be completely
    // removed soon
    // public function getFileExistsFlag()
    // {
    // return $this->_file->getDbFileExists();
    // }

    public function getFileOwnerId()
    {
        return $this->_file->getDbOwnerId();
    }

    public static function setIsPlaylist($p_playlistItems, $p_type, $p_status)
    {
        foreach ($p_playlistItems as $item) {
            $file = self::RecallById($item->getDbFileId());
            $fileId = $file->_file->getDbId();
            if ($p_type == 'playlist') {
                // we have to check if the file is in another playlist before
                // we can update
                if (!is_null($fileId) && !in_array($fileId, Application_Model_Playlist::getAllPlaylistFiles())) {
                    $file->_file->setDbIsPlaylist($p_status)->save();
                }
            } elseif ($p_type == 'block') {
                if (!is_null($fileId) && !in_array($fileId, Application_Model_Block::getAllBlockFiles())) {
                    $file->_file->setDbIsPlaylist($p_status)->save();
                }
            }
        }
    }

    public static function setIsScheduled($fileId, $status)
    {
        $file = self::RecallById($fileId);
        $updateIsScheduled = false;

        if (!is_null($fileId) && !in_array(
            $fileId,
            Application_Model_Schedule::getAllFutureScheduledFiles()
        )) {
            $file->_file->setDbIsScheduled($status)->save();
            $updateIsScheduled = true;
        }

        return $updateIsScheduled;
    }

    /**
     * Updates the is_scheduled flag to false for tracks that are no longer
     * scheduled in the future. We do this by checking the difference between
     * all files scheduled in the future and all files with is_scheduled = true.
     * The difference of the two result sets is what we need to update.
     */
    public static function updatePastFilesIsScheduled()
    {
        $futureScheduledFilesSelectCriteria = new Criteria();
        $futureScheduledFilesSelectCriteria->addSelectColumn(CcSchedulePeer::FILE_ID);
        $futureScheduledFilesSelectCriteria->setDistinct();
        $futureScheduledFilesSelectCriteria->add(CcSchedulePeer::ENDS, gmdate(DEFAULT_TIMESTAMP_FORMAT), Criteria::GREATER_THAN);
        $stmt = CcSchedulePeer::doSelectStmt($futureScheduledFilesSelectCriteria);
        $filesScheduledInFuture = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);

        $filesCurrentlySetWithIsScheduledSelectCriteria = new Criteria();
        $filesCurrentlySetWithIsScheduledSelectCriteria->addSelectColumn(CcFilesPeer::ID);
        $filesCurrentlySetWithIsScheduledSelectCriteria->add(CcFilesPeer::IS_SCHEDULED, true);
        $stmt = CcFilesPeer::doSelectStmt($filesCurrentlySetWithIsScheduledSelectCriteria);
        $filesCurrentlySetWithIsScheduled = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);

        $diff = array_diff($filesCurrentlySetWithIsScheduled, $filesScheduledInFuture);

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

class DeleteScheduledFileException extends Exception {}
class FileDoesNotExistException extends Exception {}
class FileNoPermissionException extends Exception {}