<?php
/**
 * Class represents records from table resource_access
 * {autogenerated}
 * @property int $resource_access_id 
 * @property int $resource_id 
 * @property string $resource_type 
 * @property string $fn enum('product_id','product_category_id','free')
 * @property int $id 
 * @property int $start_days 
 * @property int $stop_days 
 * @see Am_Table
 */
class ResourceAccess extends Am_Record 
{
    const FN_PRODUCT = 'product_id';
    const FN_CATEGORY = 'product_category_id';
    const FN_FREE = 'free';
    const FN_FREE_WITHOUT_LOGIN = 'free_without_login';
    
    // resource_id types
    const FOLDER = 'folder';
    const PAGE = 'page';
    const LINK = 'link';
    const FILE = 'file';
    const VIDEO = 'video';
    const AUDIO = 'audio';
    const INTEGRATION = 'integration'; // plugin name must be appened
    const EMAILTEMPLATE = 'emailtemplate';
    
    /**
     * By default includes: FOLDER, PAGE, LINK, FILE, VIDEO
     */
    const USER_VISIBLE_TYPES = 'user-visible-types';
    /**
     * By default includes: FOLDER, PAGE, LINK 
     */
    const USER_VISIBLE_PAGES = 'user-visible-pages';

    const ANY_PRODUCT = -1; // special category constant

    public function getId()
    {
        return $this->id;
    }
    public function getClass()
    {
        return $this->fn;
    }
    public function getClassTitle()
    {
        switch ($this->fn)
        {
            case self::FN_FREE : return ___('Free');
            case self::FN_FREE_WITHOUT_LOGIN : return ___('Free');
            case self::FN_CATEGORY : return ___('Category');
            case self::FN_PRODUCT : return ___('Product');
            default: return $this->fn;
        };
    }
    public function getTitle()
    {
        if ($this->fn == self::FN_FREE)
            return ___('Free Access');
        if ($this->fn == self::FN_FREE_WITHOUT_LOGIN)
            return ___('Free Access without log-in');
        $pr = null;
        if ($this->id)
        {
            if ($this->fn == self::FN_PRODUCT)
                $pr = $this->getDi()->productTable->load($this->id, false);
            elseif ($this->fn == self::FN_CATEGORY)
            {
                if ($this->id == self::ANY_PRODUCT)
                    return ___('Any product');
                else
                    $pr = $this->getDi()->productCategoryTable->load($this->id, false);
            }
        }
        if (!$pr) return sprintf('(%s #%d)', $this->getClass(), $this->getId());
        return $pr->title;
    }
    public function getStart()
    {
        return strlen($this->start_days) ? "{$this->start_days}d" : null;
    }
    public function getStop($parse_forever = true)
    {
        if ($this->stop_days == -1 && $parse_forever) return "forever";
        return strlen($this->stop_days) ? "{$this->stop_days}d" : null;
    }
    public function hasCustomStartStop()
    {
        return strlen($this->stop_day) || strlen($this->start_day);
    }
    public function isAnyProducts()
    {
        return empty($this->product_id) && @$this->product_category_id <= 0;
    }
    public function isFree()
    {
        return ($this->fn == self::FN_FREE) || ($this->fn == self::FN_FREE_WITHOUT_LOGIN);
    }
}

class ResourceAccessTable extends Am_Table {
    protected $_key = 'resource_access_id';
    protected $_table = '?_resource_access';
    protected $_recordClass = 'ResourceAccess';

    /**
     * @return Am_Query
     */
    protected function _getBaseQuery($joinConditions = "")
    {
        if ($joinConditions)
            $joinConditions = "(" . $joinConditions . ") AND ";
        $q = new Am_Query($this, 'r');
        $q->clearFields();
        $q->addField('DISTINCT r.resource_id', 'resource_id');
        $q->addField('resource_type', 'resource_type');
        $q->leftJoin("?_access_cache", "c", 
                    "
                    $joinConditions 
                    (((c.fn = r.fn) AND (c.id = r.id)) OR (r.fn='product_category_id' AND r.id=-1)) 
                    AND (
                            (c.status='active' AND r.start_days IS NULL AND r.stop_days IS NULL) 
                            OR
                            (c.status='active' AND c.days BETWEEN IFNULL(r.start_days,0) AND IFNULL(r.stop_days, 90000)) 
                            OR
                            (c.days >= IFNULL(r.start_days,0) AND r.stop_days = -1) 
                       )");
        // is available if free, or if user has equal subscription record in access_cache
        $q->addWhere("(r.fn IN ('free', 'free_without_login') OR c.user_id IS NOT NULL)");
        return $q;
    }
    
    /**
     * Return resources currently allowed for user
     * @param User $user
     * @param array|single type constant from ResourceAccess $types
     * @return array of records (as array)
     */
    function selectAllowedResources(User $user, $types = null)
    {
        // select product_id/product_category_id, type, number of days
        $q = $this->_getBaseQuery("c.user_id=".intval($user->pk()));
        if ($types !== null)
            $q->addWhere("resource_type IN (?a)", is_array($types) ? $types : array($types));
        return $this->_db->fetchRows($q->query());
    }
    /**
     * Return allowed resources as objects
     * @return array of ResourceAbstract
     * @see self::selectAllowedResources
     */
    function getAllowedResources(User $user, $types = null, $groupByType = true)
    {
        $ret = array();
        if ($types === ResourceAccess::USER_VISIBLE_TYPES)
            $types = array(
                ResourceAccess::FOLDER,
                ResourceAccess::FILE,
                ResourceAccess::PAGE,
                ResourceAccess::LINK,
                ResourceAccess::VIDEO,
            );
        elseif ($types === ResourceAccess::USER_VISIBLE_PAGES)
            $types = array(
                ResourceAccess::FOLDER,
                ResourceAccess::PAGE,
                ResourceAccess::LINK,
            );
        
        $res = $this->selectAllowedResources($user, $types);
        usort($res, array($this, '_sortByResourceType'));
        foreach ($res as $r)
        {
            $o = $this->getDi()->getService(lcfirst(toCamelCase($r['resource_type'])) . 'Table')->load($r['resource_id'], false);
            if ($o) $ret[] = $o;
        }
        /** @todo - optimize: load resources by same type with one query */
        return $ret;
    }
    private function _sortByResourceType($a, $b)
    {
        $pri = array(
            ResourceAccess::FOLDER => 3,
            ResourceAccess::PAGE => 2,
            ResourceAccess::FILE => 1,
        );
        $pria = intval(@$pri[$a['resource_type']]);
        $prib = intval(@$pri[$b['resource_type']]);
        return -strcmp($pria, $prib);
    }
    function userHasAccess(User $user, $id, $type)
    {
        $q = $this->_getBaseQuery("c.user_id=".intval($user->pk()));
        $q->addWhere("resource_type=?", $type);
        $q->addWhere("resource_id=?", $id);
        return (bool)$q->selectPageRecords(0, 1);
    }
    /** 
     * Return true if not logged-in visitor has access
     * to the resource
     */
    function guestHasAccess($id, $type)
    {
        return (bool)$this->_db->selectCell("SELECT resource_access_id
            FROM {$this->_table}
            WHERE resource_type=? AND resource_id=?d AND fn=?", 
                $type, $id, ResourceAccess::FN_FREE_WITHOUT_LOGIN);
    }
    
    function updateCache($userId = null)
    {
        if ($userId === null)
        {
            $this->_db->query("ALTER TABLE ?_access_cache DISABLE KEYS");
            $this->_db->query("TRUNCATE TABLE ?_access_cache");
        } else
            $this->_db->query("DELETE FROM ?_access_cache {WHERE user_id=?d}", $userId ? $userId : DBSIMPLE_SKIP);
        
        $productCatsCache = $this->getDi()->productCategoryTable->getCategoryProducts();
        $dat = $this->getDi()->sqlDate;
        $today = $this->getDi()->sqlDate;
        
        $q = $this->_db->queryResultOnly("
            SELECT 
            user_id, 
            product_id, 
            UNIX_TIMESTAMP(begin_date) AS begin_date, 
            UNIX_TIMESTAMP(LEAST(?, expire_date)) AS expire_date
            ,begin_date AS sql_begin_date
            ,expire_date AS sql_expire_date
            FROM ?_access 
            WHERE {user_id = ?d AND } begin_date <= ? 
            ORDER BY user_id
        ", 
            $this->getDi()->sqlDate,
            $userId ? $userId : DBSIMPLE_SKIP,
            $this->getDi()->sqlDate);
        $rows = array();
        $insert = array();
        $lastUserId = null;
        while ($r = $this->_db->fetchRow($q))
        {
            if (($r['user_id'] != $lastUserId) && $rows)
            {
                $insert = array_merge($insert, $this->_updateCacheUser($rows, $productCatsCache, $dat, $today));
                if (count($insert) > 100)
                {
                    $this->_insertCache($insert);
                    $insert = array();
                }
                $rows = array();
            }
            $rows[] = $r;
            $lastUserId = $r['user_id'];
        }
        $this->_db->freeResult($q);
        if ($rows)
            $insert = array_merge($insert, $this->_updateCacheUser($rows, $productCatsCache, $dat, $today));
        if ($insert)
            $this->_insertCache($insert);
        if ($userId === null)
        {
            $this->_db->query("ALTER TABLE ?_access_cache ENABLE KEYS");
        }
    }
    function _insertCache(array $insert)
    {
        // todo - direct query : disable _expandPlaceholdersCallback
        $prefix = $this->_db->getPrefix();
        $this->_db->queryQuick("INSERT INTO {$prefix}access_cache 
            (user_id, fn, id, days, begin_date, expire_date, status)
            VALUES\n" .
            implode(",", $insert));
    }
    function _updateCacheUser(array $rows, array $productCatsCache, $dat, $today)
    {
        $active = $insert = array();
        $min = $max = array();
        foreach ($rows as $r)
        {
            $pid = $r['product_id'];
            $dates[$pid][] = array($r['begin_date']+43200, 0);
            $dates[$pid][] = array($r['expire_date']+43200, 1);
            if (empty($min[$pid]) || ($min[$pid] > $r['sql_begin_date'])) $min[$pid] = $r['sql_begin_date']; 
            if (empty($max[$pid]) || ($max[$pid] < $r['sql_expire_date'])) $max[$pid] = $r['sql_expire_date']; 
            if (($r['sql_begin_date'] <= $today) && ($today <= $r['sql_expire_date']))
            {
                $active[$r['product_id']] = "active";
            } elseif (empty($active[$r['product_id']])) {
                $active[$r['product_id']] = "expired";
            }
        }
        
        $len = array();
        foreach ($dates as $pid => $d) 
        {
            sort($dates[$pid]);
            $len[$pid] = $this->_calcKleeLen($dates[$pid]);
            $insert[] = sprintf("(%d,'product_id',%d,%d,'%s','%s','%s')", 
                $r['user_id'], $pid, 
                $len[$pid], $min[$pid], $max[$pid], $active[$pid]
            );
        }
        foreach ($productCatsCache as $pc => $pids)
        {
            $catDates = array();
            $catActive = 'expired';
            $catMax = $catMin = array();
            foreach ($pids as $pid) 
            {
                if (!empty($dates[$pid]))
                {
                    $catDates = array_merge($catDates, $dates[$pid]);
                    if ($active[$pid] == 'active')
                        $catActive = 'active';
                    $catMin[$pid] = $min[$pid];
                    $catMax[$pid] = $max[$pid];
                }
            }
            if (!$catDates) continue;
            if (count($catMax) == 1) // if there's only one product found
            {
                $pid = key($catMax);
                $insert[] = sprintf("(%d,'product_category_id',%d,%d,'%s','%s','%s')", 
                    $r['user_id'], $pc, 
                    $len[$pid], $min[$pid], $max[$pid], $active[$pid]);
            } else {
                sort($catDates);
                $insert[] = sprintf("(%d,'product_category_id',%d,%d,'%s','%s','%s')", 
                    $r['user_id'], $pc, 
                    $this->_calcKleeLen($catDates), min($catMin), max($catMax), $catActive
                );
            }
        }
        return $insert;
    }
    function _calcKleeLen($datesArray)
    {
        $len = 0;
        $c = 0;
        foreach ($datesArray as $i => $v)
        {
            if ($c && $i) // count only inside an interval
            {
                $len += $v[0] - $datesArray[$i-1][0];
            }
            if ($v[1]) // if end
                --$c; // we have finished an interval
            else {
                if (!$c) // opens new interval
                    $len += 86400;
                ++$c; // we have started a new interval
            }
        }
        return round($len/86400);
    }
    
    /**
     * select resource accessible for customers using
     * records (user_id, resource_id, resource_type, login, email)
     * @return Am_Query
     */
    function getResourcesForMembers($types = null, $condition="1=1")
    {
        if ($types && !is_array($types))
            $types = (array)$types;
        
        $qfree = new Am_Query($this, 'rfree');
        $qfree->crossJoin('?_user', 'u')
            ->clearFields()
            ->addField('u.user_id')
            ->addField('rfree.resource_id')
            ->addField('rfree.resource_type')
            ->addField('u.login')
            ->addField('u.email')
            ->groupBy('user_id, resource_id, resource_type', 'u')
            ->addWhere("rfree.fn IN ('free', 'free_without_login')")
            ->addWhere("(
                            (rfree.start_days IS NULL AND rfree.stop_days IS NULL) 
                            OR
                            (CEIL((UNIX_TIMESTAMP() - UNIX_TIMESTAMP(u.added))/86400) BETWEEN IFNULL(rfree.start_days,0) AND IFNULL(rfree.stop_days, 90000)) 
                            OR
                            (CEIL((UNIX_TIMESTAMP() - UNIX_TIMESTAMP(u.added))/86400) >= IFNULL(rfree.start_days,0) AND rfree.stop_days = -1) 
                       )");
        if ($types)
            $qfree->addWhere('rfree.resource_type IN (?a) AND ' . $condition, $types);

        $q = $this->_getBaseQuery();
        $q->clearFields();
        $q->addField('DISTINCT c.user_id')
            ->addField('r.resource_id')
            ->addField('r.resource_type')
            ->addField('u.login')
            ->addField('u.email')
            ->leftJoin('?_user', 'u', 'u.user_id=c.user_id')
            ->addOrder('user_id')
            ->groupBy('user_id, resource_id, resource_type', 'c')
        // we will use separate query for free records
            ->addWhere("r.fn NOT IN ('free', 'free_without_login')")
            ->addUnion($qfree);
        
        if ($types)
            $q->addWhere('r.resource_type IN (?a) AND ' . $condition, $types);
        return $q;
    }
    
    function getFnValues()
    {
        return array(
            ResourceAccess::FN_CATEGORY,
            ResourceAccess::FN_PRODUCT,
            ResourceAccess::FN_FREE,
            ResourceAccess::FN_FREE_WITHOUT_LOGIN,
        );
    }
}
