<?php

/**
 * This software is intended for use with Oxwall Free Community Software http://www.oxwall.org/ and is
 * licensed under The BSD license.

 * ---
 * Copyright (c) 2011, Oxwall Foundation
 * All rights reserved.

 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *  - Redistributions of source code must retain the above copyright notice, this list of conditions and
 *  the following disclaimer.
 *
 *  - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
 *  the following disclaimer in the documentation and/or other materials provided with the distribution.
 *
 *  - Neither the name of the Oxwall Foundation nor the names of its contributors may be used to endorse or promote products
 *  derived from this software without specific prior written permission.

 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @author Kairat Bakitow <kainisoft@gmail.com>
 * @package ow_plugins.photo.bol
 * @since 1.6.1
 */
class PHOTO_BOL_SearchService
{
    CONST SEARCH_LIMIT = 10;
    CONST CONTENT_INDEXING_LIMIT = 100;
    CONST CONTENT_INDEX_FILTER_PATTERN = '/[\w-]{{$wordLen},}/u';
    
    CONST ENTITY_TYPE_ALBUM = 'photo.album';
    CONST ENTITY_TYPE_PHOTO = 'photo.photo';
    
    private static $classInstance;
    
    private $entityTypes = array();

    private $photoDao;
    private $photoCacheDao;
    private $dataDao;
    private $indexDao;
    private $entityTypeDao;

    private function __construct()
    {
        $this->photoDao = PHOTO_BOL_PhotoDao::getInstance();
        $this->photoCacheDao = PHOTO_BOL_PhotoCacheDao::getInstance();
        $this->dataDao = PHOTO_BOL_SearchDataDao::getInstance();
        $this->indexDao = PHOTO_BOL_SearchIndexDao::getInstance();
        $this->entityTypeDao = PHOTO_BOL_SearchEntityTypeDao::getInstance();
        
        $this->reloadEntityTypes();
    }

    public static function getInstance()
    {
        if ( null === self::$classInstance )
        {
            self::$classInstance = new self();
        }

        return self::$classInstance;
    }
    
    private function reloadEntityTypes()
    {
        $this->entityTypes = array();
        
        foreach ( $this->entityTypeDao->findAll() as $entityType )
        {
            $this->entityTypes[$entityType->entityType] = (int)$entityType->id;
        }
    }
    
    public function addSearchData( $entityId, $entityType, $content )
    {
        if ( empty($entityId) || empty($entityType) || empty($content) )
        {
            return;
        }
        
        if ( !array_key_exists($entityType, $this->entityTypes) )
        {
            return;
        }
        
        $this->deleteSearchItem($entityType, $entityId);
        
        $entity = new PHOTO_BOL_SearchData();
        $entity->entityTypeId = $this->entityTypes[$entityType];
        $entity->entityId = $entityId;
        $entity->content = $content;
        
        $this->dataDao->save($entity);
    }

    public function contentIndexing()
    {
        $indexedIds = array();
        $len = $this->indexDao->getMinWordLen();
        $pattern = str_replace('{$wordLen}', $len, self::CONTENT_INDEX_FILTER_PATTERN);
        
        foreach ( $this->dataDao->getDataForIndexing(self::CONTENT_INDEXING_LIMIT) as $data )
        {
            $match = array();

            if ( preg_match_all($pattern, html_entity_decode($data->content, ENT_QUOTES, 'UTF-8'), $match) )
            {
                $index = new PHOTO_BOL_SearchIndex();
                $index->entityTypeId = (int)$data->entityTypeId;
                $index->entityId = (int)$data->entityId;
                $index->content = implode(' ', $match[0]);
                
                try
                {
                    $this->indexDao->save($index);
                }
                catch ( Exception $e ) { }
            }
            
            $indexedIds[] = $data->id;
        }
        
        $this->dataDao->deleteByIdList($indexedIds);
    }
    
    public function addSearchIndex( $entityType, $entityId, $content )
    {
        if ( empty($entityId) || empty($entityType) || empty($content) )
        {
            return;
        }
        
        if ( !array_key_exists($entityType, $this->entityTypes) )
        {
            return;
        }
        
        $len = $this->indexDao->getMinWordLen();
        $pattern = str_replace('{$wordLen}', $len, self::CONTENT_INDEX_FILTER_PATTERN);
        $match = NULL;
        
        if ( preg_match_all($pattern, html_entity_decode($content, ENT_QUOTES, 'UTF-8'), $match) )
        {
            $index = new PHOTO_BOL_SearchIndex();
            $index->entityTypeId = $this->entityTypes[$entityType];
            $index->entityId = (int)$entityId;
            $index->content = implode(' ', $match[0]);

            try
            {
                $this->indexDao->save($index);
                
                return TRUE;
            }
            catch ( Exception $e )
            {
                return FALSE;
            }
        }
    }

    public function getEntityTypeId( $entityType )
    {
        return (empty($entityType) || !isset($this->entityTypes[$entityType])) ? NULL : $this->entityTypes[$entityType];
    }

    public function addEntityType( $entityType )
    {
        if ( empty($entityType) || $this->getEntityTypeId($entityType) !== NULL )
        {
            return FALSE;
        }
        
        $entity = new PHOTO_BOL_SearchEntityType();
        $entity->entityType = $entityType;
        $this->entityTypeDao->save($entity);
        
        $this->reloadEntityTypes();
        
        return $entity;
    }
    
    public function deleteSearchItem( $entityType, $entityId )
    {
        if ( !array_key_exists($entityType, $this->entityTypes) )
        {
            return FALSE;
        }
        
        if ( !$this->indexDao->deleteIndexItem($this->entityTypes[$entityType], $entityId) )
        {
            $this->dataDao->deleteDataItem($this->entityTypes[$entityType], $entityId);
        }
        
        return TRUE;
    }
    
    public function getSearchResultListByTag( $tag, $limit = self::SEARCH_LIMIT )
    {
        $result = array();
        
        foreach ( $this->photoDao->getSearchResultListByTag($tag, $limit) as $list )
        {
            $result[$list['id']] = $list;
        }
        
        return $result;
    }
    
    public function getSearchResultAllListByTag( $tag )
    {
        return $this->photoDao->getSearchResultAllListByTag($tag);
    }
    
    public function getSearchResultListByUsername( $username, $limit = self::SEARCH_LIMIT )
    {
        $questionName = OW::getConfig()->getValue('base', 'display_name_question');
        
        $questionValues = array($questionName => $username );
        $idList = BOL_UserService::getInstance()->findUserIdListByQuestionValues($questionValues, 0, $limit);
        
        $resultList = $this->photoDao->getSearchResultListByUserIdList($idList, $limit);
        $displayNameList = BOL_UserService::getInstance()->getDisplayNamesForList($idList);
        
        foreach ( $resultList as $key => $list )
        {
            $resultList[$key]['label'] = $displayNameList[$list['id']];
        }
        
        return $resultList;
    }
    
    public function getSearchResultAllListByUsername( $username )
    {
        $questionName = OW::getConfig()->getValue('base', 'display_name_question');
        
        $questionValues = array($questionName => $username );
        $limit = BOL_UserService::getInstance()->countUsersByQuestionValues($questionValues);
        
        return BOL_UserService::getInstance()->findUserIdListByQuestionValues($questionValues, 0, $limit);
    }
    
    public function getSearchResultListByDescription( $description, $limit = self::SEARCH_LIMIT )
    {
        $list = $this->photoDao->getSearchResultListByDescription($description, $limit);
        $result = array();
        
        foreach ( $list as $val )
        {
            if ( mb_strlen($val['label']) > 50 )
            {
                $i = mb_stripos($val['label'], $description);
                $val['label'] = '...' . mb_substr($val['label'], ($i - 10) < 0 ? 0 : $i - 10, 50) . '...';
            }
            
            $result[$val['id']] = $val;
        }
        
        return $result;
    }
    
    public function getSearchResultAllListByDescription( $description )
    {
        return $this->photoDao->getSearchResultAllListByDescription($description);
    }
    
    public function getSearchResult( $searchVal, $limit = self::SEARCH_LIMIT )
    {
        if ( strlen($searchVal = trim($searchVal)) === 0 )
        {
            return array();
        }
        
        if ( ($cache = $this->photoCacheDao->findCacheByKey($this->photoCacheDao->getKey($searchVal))) !== NULL )
        {
            return json_decode($cache->data, TRUE);
        }
        
        if ( preg_match('/^(?:#|@)\S+/', $searchVal) === 1 )
        {
            switch ( $searchVal[0] )
            {
                case '#':
                    $list = $this->getSearchResultListByTag($searchVal, $limit);
                    $result = array('result' => TRUE, 'type' => 'hash', 'list' => $list);
                    break;
                case '@':
                    $_result = $this->getSearchResultListByUsername(trim($searchVal, '@'), self::SEARCH_LIMIT);
                    $userIdList = array();
                    $list = array();
                    
                    foreach ( $_result as $val )
                    {
                        $userIdList[] = $val['id'];
                        $list[$val['id']] = $val;
                    }

                    $result = array( 
                        'type' => 'user', 
                        'list' => $list,
                        'avatarData' => BOL_AvatarService::getInstance()->getDataForUserAvatars($userIdList, TRUE, FALSE, FALSE, FALSE)
                    );
                    break;
            }
        }
        else
        {
            if ( mb_strlen($searchVal) < $this->indexDao->getMinWordLen() )
            {
                $list = $this->getSearchResultListByDescription($searchVal, self::SEARCH_LIMIT);
            }
            else
            {
                $list = array();
                $arr = array();
                
                foreach ( explode(' ', $searchVal) as $val )
                {
                    $arr[] = '+' . trim($val) . '*';
                }
                
                $data = $this->indexDao->findIndexedData(implode(' ', $arr), array(self::ENTITY_TYPE_PHOTO));
                $arrHash = array();
        
                foreach ( $data as $index )
                {
                    $label = $index->content;

                    if ( mb_strlen($label) > 50 )
                    {
                        $i = mb_stripos($label, $searchVal);
                        $label = '...' . mb_substr($label, ($i - 10) < 0 ? 0 : $i - 10, 50) . '...';
                    }

                    $hash = md5($label);

                    if ( array_key_exists($hash, $arrHash) === FALSE )
                    {
                        $list[$index->entityId] = array('id' => $index->entityId, 'label' => $label, 'count' => 1, 'ids' => $index->entityId);
                        $arrHash[$hash] = $index->entityId;
                    }
                    else
                    {
                        $list[$arrHash[$hash]]['count']++;
                        $list[$arrHash[$hash]]['ids'] .= ',' . $index->entityId;
                    }
                }
            }
            
            $result = array('type' => 'desc', 'list' => $list);
        }

        try
        {
            $cache = new PHOTO_BOL_PhotoCache();
            $cache->key = $this->photoCacheDao->getKey($searchVal);
            $cache->data = json_encode($result);
            $cache->createTimestamp = time();
            $this->photoCacheDao->save($cache);
        }
        catch ( Exception $e ){}
        
        return $result;
    }
    
    public function getSearchAllResult( $searchVal )
    {
        if ( empty($searchVal) )
        {
            return array('result' => TRUE, 'list' => array());
        }
        
        if ( ($cache = $this->photoCacheDao->findCacheByKey($this->photoCacheDao->getKeyAll($searchVal))) !== NULL )
        {
            $data = json_decode($cache->data);
            $ids = $data->ids;
            $type = $data->type;
        }
        else
        {
            $ids = array();

            if ( preg_match('/^(?:#|@)\S+/', $searchVal) === 1 )
            {
                switch ( $searchVal[0] )
                {
                    case '#':
                        $tagIdList = $this->photoDao->getSearchResultAllListByTag($searchVal);
                        $ids = $this->photoDao->getPhotoIdListByTagIdList($tagIdList);
                        $type = 'hash';
                        break;
                    case '@':
                        $userIdList = $this->getSearchResultAllListByUsername(trim($searchVal, '@'));
                        $ids = $this->photoDao->findPhotoIdListByUserIdList($userIdList);
                        $type = 'user';
                        break;
                }
            }
            else
            {
                if ( mb_strlen($searchVal) < PHOTO_BOL_SearchIndexDao::getInstance()->getMinWordLen() )
                {
                    $ids = $this->photoDao->getSearchResultAllListByDescription($searchVal);
                }
                else
                {
                    $arr = array();

                    foreach ( explode(' ', $searchVal) as $val )
                    {
                        $arr[] = '+' . trim($val) . '*';
                    }

                    $data = $this->indexDao->findIndexedData(implode(' ', $arr), array(self::ENTITY_TYPE_PHOTO));

                    foreach ( $data as $index )
                    {
                        $ids[] = $index->entityId;
                    }
                }
                
                $type = 'desc';
            }
            
            try
            {
                $cache = new PHOTO_BOL_PhotoCache();
                $cache->key = $this->photoCacheDao->getKeyAll($searchVal);
                $cache->data = json_encode(array('type' => $type, 'ids' => $ids));
                $cache->createTimestamp = time();
                $this->photoCacheDao->save($cache);
            }
            catch ( Exception $e ){}
        }
        
        return array('type' => $type, 'ids' => $ids);
    }
    
    public function getResultIdList( $searchVal, $id, $listType )
    {
        static $listArr = array();
        
        if ( array_key_exists(($crc = crc32($searchVal)), $listArr) )
        {
            return $listArr[$crc];
        }
        
        if ( $listType == 'all' )
        {
            if ( ($cache = $this->photoCacheDao->findCacheByKey($this->photoCacheDao->getKeyAll($searchVal))) !== NULL )
            {
                $cache->createTimestamp = time();
                $this->photoCacheDao->save($cache);

                $listCache = json_decode($cache->data, TRUE);
                $list = $listCache['ids'];
            }
            else
            {
                $result = $this->getSearchAllResult($searchVal);
                $list = $result['ids'];
            }
        }
        else
        {
            if ( ($cache = $this->photoCacheDao->findCacheByKey($this->photoCacheDao->getKey($searchVal))) !== NULL )
            {
                $cache->createTimestamp = time();
                $this->photoCacheDao->save($cache);

                $listCache =  json_decode($cache->data, TRUE);
                $list = explode(',', $listCache['list'][$id]['ids']);
            }
            else
            {
                $result = $this->getSearchResult($searchVal);
                $list = explode(',', $result['list'][$id]['ids']);
            }
        }
        
        $listArr[$crc] = $list;
        
        return $list;
    }

    public function getPrevPhotoIdList( $listType, $photoId, $data )
    {
        return $this->getResultIdList($data['searchVal'], $data['id'], $listType);
    }

    public function getFirstPhotoIdList( $listType, $photoId, $data )
    {
        return $this->getResultIdList($data['searchVal'], $data['id'], $listType);
    }
    
    public function getNextPhotoIdList( $listType, $photoId, $data )
    {
        return $this->getResultIdList($data['searchVal'], $data['id'], $listType);
    }
    
    public function getLastPhotoIdList( $listType, $photoId, $data )
    {
        return $this->getResultIdList($data['searchVal'], $data['id'], $listType);
    }
}
