/*
 * Copyright (C) 2003-2013 eXo Platform SAS.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.exoplatform.calendar.service.impl;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.query.Query;
import javax.jcr.query.Row;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

import org.apache.commons.collections.map.HashedMap;
import org.exoplatform.calendar.service.Calendar;
import org.exoplatform.calendar.service.CalendarEvent;
import org.exoplatform.calendar.service.CalendarService;
import org.exoplatform.calendar.service.EventQuery;
import org.exoplatform.calendar.service.GroupCalendarData;
import org.exoplatform.calendar.service.Utils;
import org.exoplatform.calendar.service.CalendarSetting;
import org.exoplatform.commons.api.search.SearchServiceConnector;
import org.exoplatform.commons.api.search.data.SearchContext;
import org.exoplatform.commons.api.search.data.SearchResult;
import org.exoplatform.commons.utils.ListAccess;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.portal.mop.SiteKey;
import org.exoplatform.portal.mop.SiteType;
import org.exoplatform.portal.mop.navigation.NavigationContext;
import org.exoplatform.portal.mop.navigation.NavigationService;
import org.exoplatform.portal.mop.navigation.NodeContext;
import org.exoplatform.portal.mop.navigation.NodeModel;
import org.exoplatform.portal.mop.navigation.Scope;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.security.ConversationState;
import org.exoplatform.services.security.Identity;
import org.exoplatform.social.core.space.model.Space;
import org.exoplatform.social.core.space.spi.SpaceService;
import org.exoplatform.web.controller.QualifiedName;
import org.exoplatform.web.controller.router.Router;

/**
 * Created by The eXo Platform SAS
 * Author : eXoPlatform
 *          exo@exoplatform.com
 * Jan 22, 2013  
 */
public class CalendarSearchServiceConnector extends SearchServiceConnector {

  private CalendarService calendarService_;
  private SpaceService spaceService_;

  private static final Log     log                 = ExoLogger.getLogger(CalendarSearchServiceConnector.class);
  private ThreadLocal<Map<String, Calendar>> calendarMap = new ThreadLocal<Map<String, Calendar>>();

  public CalendarSearchServiceConnector(InitParams initParams) {
    super(initParams);
    calendarService_  = (CalendarService) ExoContainerContext.getCurrentContainer().getComponentInstanceOfType(CalendarServiceImpl.class);
    spaceService_ = (SpaceService) ExoContainerContext.getCurrentContainer().getComponentInstanceOfType(SpaceService.class);
  }

  @Override
  public Collection<SearchResult> search(SearchContext context,
                                         String query,
                                         Collection<String> sites,
                                         int offset,
                                         int limit,
                                         String sort,
                                         String order) {
    return searchData(context, null, query, sites, offset, limit, sort, order);

  }


  protected Collection<SearchResult> searchData(SearchContext context, String dataType, String query,
                                                Collection<String> sites,
                                                int offset,
                                                int limit,
                                                String sort,
                                                String order) {
    List<SearchResult> events = new ArrayList<SearchResult>();
    if(Utils.isEmpty(query)) {
      return events;
    }

    SessionProvider provider = SessionProvider.createSystemProvider();
    OrganizationService orgService = (OrganizationService)ExoContainerContext.getCurrentContainer()
              .getComponentInstanceOfType(OrganizationService.class);    
    Set<String> readOnlyCalendars = new HashSet<String>();
    
    try {
      getCalendarMap().clear();
      Identity currentUser = ConversationState.getCurrent().getIdentity(); 
      final String userId = currentUser.getUserId();
      
      List<String> uCals = new LinkedList<String>();
      List<Calendar> privateCalendars = calendarService_.getUserCalendars(userId, true);
      for(Calendar cal : privateCalendars) {
        getCalendarMap().put(cal.getId(), cal);
        uCals.add(cal.getId());
      }

      List<String> sCals = new LinkedList<String>();
      GroupCalendarData sharedCalendar = calendarService_.getSharedCalendars(userId, true) ;
      if(sharedCalendar != null) {
        List<Calendar> shareCalendars = sharedCalendar.getCalendars();
        for(Calendar cal : shareCalendars){
          getCalendarMap().put(cal.getId(), cal);          
          sCals.add(cal.getId());
          
          if(!Utils.canEdit(orgService, cal.getEditPermission(), userId)) {
            readOnlyCalendars.add(cal.getId());
          }
        }
      }

      List<String> gCals = new LinkedList<String>();
      Set<String> groupIds = currentUser.getGroups();
      if(!groupIds.isEmpty()) {
        List<GroupCalendarData> groupCalendar = calendarService_.getGroupCalendars(groupIds.toArray(new String[groupIds.size()]), true, userId) ;
        if(groupCalendar != null) {
          List<Calendar> spaceCalendars = new ArrayList<Calendar>();
          for(GroupCalendarData gCal : groupCalendar) {
            if(gCal.getCalendars() != null) spaceCalendars.addAll(gCal.getCalendars());
          }
          for(Calendar cal : spaceCalendars) {
            getCalendarMap().put(cal.getId(), cal);
            gCals.add(cal.getId());
            
            if(!Utils.canEdit(orgService, cal.getEditPermission(), userId)) {
              readOnlyCalendars.add(cal.getId());
            }
          }
        }
      }
      
      List<String> calIds = new LinkedList<String>();
      calIds.addAll(uCals);
      calIds.addAll(gCals);
      calIds.addAll(sCals);
      
      EventQuery uEventQuery = createQuery(dataType, query, sort, order, calIds.toArray(new String[calIds.size()]), readOnlyCalendars);
      ListAccess<?> listAccess = new EventRowListAccess((EventDAOImpl)calendarService_.getEventDAO(), uEventQuery);

      Object[] rows = listAccess.load(offset, limit);
      CalendarSetting calSetting = calendarService_.getCalendarSetting(userId);

      for (Object row : rows) {
        SearchResult rs = buildResult(context, sites, dataType, row, calSetting);
        if(rs != null) events.add(rs);
      }
    }
    catch (Exception e) {
      if (log.isDebugEnabled()) log.debug("Could not execute unified seach " + dataType , e) ; 
    } finally {
      provider.close();
    }
    return events;
  }

  private EventQuery createQuery(String dataType, String queryText, String sort, String order, String[] calendarIds, Set<String> excludePrivateEventInCalendars) throws Exception {
    UnifiedQuery eventQuery = new UnifiedQuery();
    eventQuery.setExcludePrivateEventInCalendars(excludePrivateEventInCalendars);
    java.util.Calendar today = java.util.Calendar.getInstance();
    eventQuery.setFromDate(today) ;
    eventQuery.setQueryType(Query.SQL);
    eventQuery.setEventType(dataType);
    eventQuery.setText(queryText) ;
    String sortBy =  Utils.SORT_FIELD_MAP.get(sort);
    if(Utils.ORDERBY_DATE.equals(sortBy)) {
      if(CalendarEvent.TYPE_EVENT.equals(dataType))
        sortBy = Utils.EXO_FROM_DATE_TIME ;
      else sortBy = Utils.EXO_TO_DATE_TIME ;
    }
    eventQuery.setOrderBy(new String[]{sortBy});
    eventQuery.setOrderType(order);
    if(CalendarEvent.TYPE_TASK.equals(dataType))
      eventQuery.setState(CalendarEvent.COMPLETED + Utils.COLON + CalendarEvent.CANCELLED);
    eventQuery.setCalendarId(calendarIds);
    
    return eventQuery;
  }


  private SearchResult buildResult(SearchContext sc, Collection<String> siteKeys, String dataType, Object iter, CalendarSetting calSeting) {
    try {
      String calId = null;
      if(iter instanceof Row) {
        Row row = (Row) iter;
        calId = row.getValue(Utils.EXO_CALENDAR_ID).getString() ;
      } else {
        Node eventNode = (Node) iter;
        if(eventNode.hasProperty(Utils.EXO_CALENDAR_ID))
          calId = eventNode.getProperty(Utils.EXO_CALENDAR_ID).getString() ;
      }
      if(getCalendarMap().keySet().contains(calId)) {
        Calendar cal = getCalendarMap().get(calId);

        StringBuffer detail = new StringBuffer();
        String title = buildValue(Utils.EXO_SUMMARY, iter);
        detail.append(cal.getName());
        String url = buildLink(sc,siteKeys, calId, buildValue(Utils.EXO_ID, iter));
        String excerpt = buildValue(Utils.EXO_DESCRIPTION, iter);
        String detailValue = Utils.EMPTY_STR;
        String imageUrl = buildImageUrl(iter);
        detail.append(buildDetail(iter, calSeting.getTimeZone()));
        if(detail.length() > 0) detailValue = detail.toString();
        long relevancy = buildScore(iter);
        long date = buildDate(iter);
        TimeZone userTimezone = TimeZone.getTimeZone(calSeting.getTimeZone());
        CalendarSearchResult result = new CalendarSearchResult(url, title, excerpt, detailValue, imageUrl, date, relevancy);
        result.setDataType(dataType);
        result.setTimeZoneName(calSeting.getTimeZone());
        result.setTimeZoneOffset(userTimezone.getOffset(date));
        if(CalendarEvent.TYPE_EVENT.equals(dataType)) {
          result.setFromDateTime(buildDate(iter, Utils.EXO_FROM_DATE_TIME).getTimeInMillis());
        } else if (CalendarEvent.TYPE_TASK.equals(dataType)) {
          result.setTaskStatus(buildValue(Utils.EXO_EVENT_STATE, iter));
        }
        return result;
      }
    }catch (Exception e) {
      if (log.isDebugEnabled())  log.debug("Error when building result object from result data " + e);
    }
    return null;
  }

  private String buildExcerpt(Object iter) throws RepositoryException{
    StringBuffer origin = new StringBuffer(Utils.EMPTY_STR);
    try {
      if(iter instanceof Row){
        Row row = (Row) iter;
        int counter = 0 ;
        for(String field : Utils.SEARCH_FIELDS)
          if(row.getValue(field) != null) {
            if(counter > 0) origin.append(Utils.SPACE);
            origin.append(row.getValue(field).getString());
            counter++;
          }
      } else {
        Node eventNode = (Node) iter;
        int counter = 0 ;
        for(String field : Utils.SEARCH_FIELDS)
          if(eventNode.hasProperty(field)){
            if(counter > 0) origin.append(Utils.SPACE);
            origin.append(eventNode.getProperty(field).getString());
            counter++;
          }
      }
    } catch (Exception e) {
      if (log.isDebugEnabled()) log.debug("Error when building customer exerpt property from data " + e);
    }
    return origin.toString();
  }

  private String buildImageUrl(Object iter) throws RepositoryException{
    String icon = null;
    if(iter instanceof Row) {
      Row row = (Row) iter;
      if(row.getValue(Utils.EXO_EVENT_TYPE) != null)
        if(CalendarEvent.TYPE_TASK.equals(row.getValue(Utils.EXO_EVENT_TYPE).getString())) 
          icon = Utils.TASK_ICON_URL;
        else icon = Utils.EVENT_ICON_URL; 
    } else {
      Node eventNode = (Node) iter;
      if(eventNode.hasProperty(Utils.EXO_EVENT_TYPE)) {
        if(CalendarEvent.TYPE_TASK.equals(eventNode.getProperty(Utils.EXO_EVENT_TYPE).getString())) 
          icon = Utils.TASK_ICON_URL;
        else icon = Utils.EVENT_ICON_URL;
      }
    }
    return icon;
  }

  private long buildDate(Object iter) {
    try {
      return buildDate(iter, Utils.EXO_DATE_CREATED).getTimeInMillis();
    } catch (NullPointerException e) {
      if (log.isDebugEnabled()) log.debug("Clould not build date value to long from data " + e);
      return 0;
    }
  }

  private java.util.Calendar buildDate(Object iter, String readProperty){
    try {
      if(iter instanceof Row){
        Row row = (Row) iter;
        return row.getValue(readProperty).getDate();
      } else {
        Node eventNode = (Node) iter;
        if(eventNode.hasProperty(readProperty)){
          return eventNode.getProperty(readProperty).getDate();
        } else {
          return null ;
        }
      }
    } catch (Exception e) {
      if (log.isDebugEnabled()) log.debug("Could not build date value from " + readProperty + " : " + e);
      return null;
    }
  }

  private long buildScore(Object iter) {
    try {
      if(iter instanceof Row){
        Row row = (Row) iter;
        return row.getValue(Utils.JCR_SCORE).getLong() ;
      }
    } catch (Exception e) {
      if (log.isDebugEnabled()) log.debug("No score return by query " + e);
    }
    return 0;
  }

  private String buildValue(String property, Object iter) throws RepositoryException{
    if(iter instanceof Row){
      Row row = (Row) iter;
      if(row.getValue(property) != null) return row.getValue(property).getString() ;
    } else {
      Node eventNode = (Node) iter;
      if(eventNode.hasProperty(property)){
        return eventNode.getProperty(property).getString();
      }
    } 
    return Utils.EMPTY_STR;
  }

  private String buildDetail(Object iter, String timeZone) throws RepositoryException{
    SimpleDateFormat df = new SimpleDateFormat(Utils.DATE_TIME_FORMAT) ;
    df.setTimeZone(TimeZone.getTimeZone(timeZone));
    StringBuffer detail = new StringBuffer();
    if(iter instanceof Row){
      Row row = (Row) iter;
      if(row.getValue(Utils.EXO_EVENT_TYPE) != null)
        if(CalendarEvent.TYPE_EVENT.equals(row.getValue(Utils.EXO_EVENT_TYPE).getString())) {
          if(row.getValue(Utils.EXO_FROM_DATE_TIME) != null)
            detail.append(Utils.SPACE).append(Utils.MINUS).append(Utils.SPACE).append(df.format(row.getValue(Utils.EXO_FROM_DATE_TIME).getDate().getTime())) ;
          if(row.getValue(Utils.EXO_LOCATION) != null)
            detail.append(Utils.SPACE).append(Utils.MINUS).append(Utils.SPACE).append(row.getValue(Utils.EXO_LOCATION).getString()) ;
        } else {
          if(row.getValue(Utils.EXO_TO_DATE_TIME) != null)
            detail.append(Utils.SPACE).append(Utils.MINUS).append(Utils.SPACE).append(Utils.DUE_FOR).append(df.format(row.getValue(Utils.EXO_TO_DATE_TIME).getDate().getTime()));
        }
    } else {
      Node eventNode = (Node) iter;
      if(eventNode.hasProperty(Utils.EXO_EVENT_TYPE)){
        if(CalendarEvent.TYPE_EVENT.equals(eventNode.getProperty(Utils.EXO_EVENT_TYPE).getString())) {
          if(eventNode.hasProperty(Utils.EXO_FROM_DATE_TIME)) {
            detail.append(Utils.SPACE).append(Utils.MINUS).append(Utils.SPACE).append(df.format(eventNode.getProperty(Utils.EXO_FROM_DATE_TIME).getDate().getTime())) ;
          }
          if(eventNode.hasProperty(Utils.EXO_LOCATION)) {
            detail.append(Utils.SPACE).append(Utils.MINUS).append(Utils.SPACE).append(eventNode.getProperty(Utils.EXO_LOCATION).getString()) ;
          }
        } else {
          if(eventNode.hasProperty(Utils.EXO_TO_DATE_TIME)) {
            detail.append(Utils.SPACE).append(Utils.MINUS).append(Utils.SPACE).append(Utils.DUE_FOR).append(df.format(eventNode.getProperty(Utils.EXO_TO_DATE_TIME).getDate().getTime())) ;
          }
        }
      }
    }  
    return detail.toString();
  }

  private String buildLink(SearchContext sc, Collection<String> siteKeys, String calendarId, String eventId) {
    String url = Utils.NONE_NAGVIGATION;
    if(sc != null)
      try {
        SiteKey siteKey = null ;
        String spaceGroupId = null;
        String siteName = sc.getSiteName() != null ? sc.getSiteName():Utils.DEFAULT_SITENAME;

        if (calendarId.indexOf(Utils.SPACE_CALENDAR_ID_SUFFIX) > 0) {
          Calendar cal = getCalendarMap().get(calendarId);

          // In case of space calendar, calendarOwner always not null
          String ownerGroup = cal.getCalendarOwner();
          String[] groups = cal.getGroups();

          // Because user can remove space-group from calendar in portal-calendar page
          // In this case, we will redirect to portal-calendar page instead of space-calendar page when user click
          // to view event-detail (keep siteKey = null) to avoid he can not see event detail
          for (String g : groups) {
            if (g.equals(ownerGroup)) {
              spaceGroupId = ownerGroup;
              siteKey = SiteKey.group(spaceGroupId);
            }
          }
        }

        if (siteKey == null) {
            siteKey = SiteKey.portal(siteName);
        }
        
        String pageName = findCalendarPageNode(siteKey);
        if(Utils.isEmpty(pageName)) {
          siteKey = SiteKey.portal(siteName);
          pageName = findCalendarPageNode(siteKey);
        }

        Router router = sc.getRouter();
        ExoContainerContext context = (ExoContainerContext)ExoContainerContext.getCurrentContainer()
            .getComponentInstanceOfType(ExoContainerContext.class);
        String handler = context.getPortalContainerName();
        url = new StringBuffer(getUrl(router, handler, siteKey.getName(), spaceGroupId, pageName)).append(Utils.SLASH).append(Utils.DETAIL_PATH).append(Utils.SLASH).append(eventId).toString();
      } catch (Exception e) {
        if (log.isDebugEnabled()) log.debug("build link error !");
      }
    return url;
  }

  private static String findCalendarPageNode(SiteKey siteKey) {
    try {
      ExoContainer container = ExoContainerContext.getCurrentContainer();
      NavigationService navService = (NavigationService) container.getComponentInstance(NavigationService.class);
      NavigationContext nav = navService.loadNavigation(siteKey);
      
      Scope scope;
      if (siteKey.getType().equals(SiteType.GROUP)) {
        scope = Scope.GRANDCHILDREN;
      } else {
        scope = Scope.CHILDREN;
      }
      NodeContext<NodeContext<?>> parentNodeCtx = navService.loadNode(NodeModel.SELF_MODEL, nav, scope, null);
      if (parentNodeCtx.getSize() >= 1) {
        Collection<NodeContext<?>> children = parentNodeCtx.getNodes();
        if (siteKey.getType() == SiteType.GROUP) {
          children = parentNodeCtx.get(0).getNodes();
        }
        Iterator<NodeContext<?>> it = children.iterator();
        NodeContext<?> child = null;
        while (it.hasNext()) {
          child = it.next();
          if (Utils.PAGE_NAGVIGATION.equals(child.getName()) || child.getName().indexOf(Utils.PORTLET_NAME) >= 0) {
            return child.getName();
          }
        }
      }
    } catch (NullPointerException e) {
      return Utils.EMPTY_STR;
    }
    return Utils.EMPTY_STR;
  }

  public String getUrl(Router router, String handler, String siteName, String spaceGroupId, String pageName) {
    HashedMap qualifiedName = new HashedMap();
    qualifiedName.put(QualifiedName.create("gtn", "handler"), handler);
    qualifiedName.put(QualifiedName.create("gtn", "path"), pageName);
    qualifiedName.put(QualifiedName.create("gtn", "lang"), "");
    if(Utils.isEmpty(spaceGroupId)) {
      qualifiedName.put(QualifiedName.create("gtn", "sitename"), siteName);
      qualifiedName.put(QualifiedName.create("gtn", "sitetype"), SiteType.PORTAL.getName());
    } else {
      String groupId = spaceGroupId.split(Utils.SLASH)[2];
      if(spaceService_ != null) {
        Space sp = spaceService_.getSpaceByGroupId(spaceGroupId);
        if(sp != null) groupId = sp.getPrettyName();
      }
      qualifiedName.put(QualifiedName.create("gtn", "sitename"), spaceGroupId.replaceAll("/", ":"));
      qualifiedName.put(QualifiedName.create("gtn", "sitetype"), SiteType.GROUP.getName());
      qualifiedName.put(QualifiedName.create("gtn", "path"), groupId + "/" + pageName);
    }
    try {
      return "/" + handler + URLDecoder.decode(router.render(qualifiedName), "UTF-8");
    } catch (UnsupportedEncodingException e) {
      return null;
    }
  }
  
  private Map<String, Calendar> getCalendarMap() {
    Map<String, Calendar> map = calendarMap.get();
    if (map == null) {
      map = new HashMap<String, Calendar>();      
      calendarMap.set(map);
    }    
    return map;
  }
}
