package org.exoplatform.social.core.storage.impl;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.lucene.queryParser.QueryParser;
import org.chromattic.api.ChromatticSession;
import org.exoplatform.commons.chromattic.ChromatticManager;
import org.exoplatform.commons.chromattic.Synchronization;
import org.exoplatform.commons.utils.CommonsUtils;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.social.common.lifecycle.SocialChromatticLifeCycle;
import org.exoplatform.social.core.activity.model.ExoSocialActivity;
import org.exoplatform.social.core.chromattic.entity.ProfileEntity;
import org.exoplatform.social.core.identity.model.Identity;
import org.exoplatform.social.core.profile.ProfileFilter;
import org.exoplatform.social.core.space.model.Space;
import org.exoplatform.social.core.storage.query.JCRProperties;
import org.exoplatform.social.core.storage.query.QueryFunction;
import org.exoplatform.social.core.storage.query.WhereExpression;

/**
 * @author <a href="mailto:alain.defrance@exoplatform.com">Alain Defrance</a>
 * @version $Revision$
 */
public class StorageUtils {
  //
  private static final Log LOG = ExoLogger.getLogger(StorageUtils.class.getName());
  //
  public static final String ASTERISK_STR = "*";
  public static final String PERCENT_STR = "%";
  public static final char   ASTERISK_CHAR = '*';
  public static final String SPACE_STR = " ";
  public static final String EMPTY_STR = "";
  public static final String SLASH_STR = "/";
  public static final String COLON_STR = ":";
  public static final String SOC_RELATIONSHIP = "soc:relationship";
  public static final String SOC_RELCEIVER    = "soc:receiver";
  public static final String SOC_SENDER       = "soc:sender";
  public static final String SOC_IGNORED      = "soc:ignored";
  public static final String SOC_FROM         = "soc:from";
  public static final String SOC_TO           = "soc:to";
  public static final String SOC_ACTIVITY_INFO = "soc:activityInfo";
  public static final String SOC_PREFIX = "soc:";
  private final static long DAY_MILISECONDS = 86400000;//a day = 24h x 60m x 60s x 1000 milisecond.
  
  private static Class<?> cls;
  
  static {
    try {
      cls = Class.forName("org.exoplatform.platform.gadget.services.LoginHistory.LoginHistoryServiceImpl");
    } catch (ClassNotFoundException e) {
      cls = null;
      LOG.error("org.exoplatform.platform.gadget.services.LoginHistory.LoginHistoryServiceImpl class not found."
          + e.getMessage());
    }
  }
  
  public static void applyFilter(final WhereExpression whereExpression, final ProfileFilter profileFilter) {
    //
    String inputName = profileFilter.getName().replace(ASTERISK_STR, PERCENT_STR);
    processUsernameSearchPattern(inputName.trim());
    String position = addAsteriskToStringInput(StringEscapeUtils.escapeHtml(profileFilter.getPosition()).trim()).replace(ASTERISK_STR, PERCENT_STR);
    inputName = inputName.isEmpty() ? ASTERISK_STR : inputName;
    String nameForSearch = inputName.replace(ASTERISK_STR, SPACE_STR);
    char firstChar = profileFilter.getFirstCharacterOfName();
    String skills = addAsteriskToStringInput(StringEscapeUtils.escapeHtml(profileFilter.getSkills()).trim()).replace(ASTERISK_STR, PERCENT_STR);
    String company = addAsteriskToStringInput(StringEscapeUtils.escapeHtml(profileFilter.getCompany()).trim()).replace(ASTERISK_STR, PERCENT_STR);

    //
    if (firstChar != '\u0000') {
      whereExpression.and().like(
          whereExpression.callFunction(QueryFunction.LOWER, ProfileEntity.lastName),
          String.valueOf(firstChar).toLowerCase() + PERCENT_STR
      );
    }
    else if (nameForSearch.trim().length() != 0) {
      whereExpression.and().startGroup()
          .like(
            whereExpression.callFunction(QueryFunction.LOWER, ProfileEntity.firstName),
            PERCENT_STR + nameForSearch.toLowerCase() + PERCENT_STR
           )
           .or().like(
            whereExpression.callFunction(QueryFunction.LOWER, ProfileEntity.lastName),
            PERCENT_STR + nameForSearch.toLowerCase() + PERCENT_STR
           )
           .or().like(
            whereExpression.callFunction(QueryFunction.LOWER, ProfileEntity.fullName),
            PERCENT_STR + nameForSearch.toLowerCase() + PERCENT_STR
           )
           .endGroup();
    }

    if (position.length() != 0) {
      whereExpression.and().like(
          whereExpression.callFunction(QueryFunction.LOWER, ProfileEntity.position),
          PERCENT_STR + position.toLowerCase() + PERCENT_STR
      );
      whereExpression.or().like(
          whereExpression.callFunction(QueryFunction.LOWER, ProfileEntity.positions),
          PERCENT_STR + position.toLowerCase() + PERCENT_STR
      );
    }

    if (skills.length() != 0) {
      whereExpression.and().like(
          whereExpression.callFunction(QueryFunction.LOWER, ProfileEntity.skills),
          PERCENT_STR + skills.toLowerCase() + PERCENT_STR
      );
    }
  
    if (company.length() != 0) {
      whereExpression.and().like(
          whereExpression.callFunction(QueryFunction.LOWER, ProfileEntity.organizations),
          PERCENT_STR + company.toLowerCase() + PERCENT_STR
      );
    }

    if (profileFilter.getAll().length() != 0) {
      String value = escapeSpecialCharacter(profileFilter.getAll());
      if (value.trim().length() > 0 || value.replaceAll(" ", "").trim().length() > 0) {
        whereExpression.and().startGroup()
            .contains(ProfileEntity.fullName, value.toLowerCase())
            .or().contains(ProfileEntity.firstName, value.toLowerCase())
            .or().contains(ProfileEntity.lastName, value.toLowerCase())
            .or().contains(ProfileEntity.position, value.toLowerCase())
            .or().contains(ProfileEntity.skills, value.toLowerCase())
            .or().contains(ProfileEntity.positions, value.toLowerCase())
            .or().contains(ProfileEntity.organizations, value.toLowerCase())
            .or().contains(ProfileEntity.jobsDescription, value.toLowerCase())
            .endGroup();
      }
    }

  }
  
  /**
   * Escape special character by using QueryParser then replace single quote character
   * 
   * @param s the string to escape
   * @return
   */
  public static String escapeSpecialCharacter(String s) {
    return QueryParser.escape(s).replace("'", "''");
  }

  public static void applyExcludes(final WhereExpression whereExpression, final List<Identity> excludedIdentityList) {

    if (excludedIdentityList != null & excludedIdentityList.size() > 0) {
      for (Identity identity : excludedIdentityList) {
        whereExpression.and().not().equals(ProfileEntity.parentId, identity.getId());
      }
    }
  }

  public static void applyWhereFromIdentity(final WhereExpression whereExpression, final List<Identity> identities) {

    //
    whereExpression.startGroup();
    if (identities.size() > 0) {
      for (int i = 0; identities.size() > i; ++i) {
        Identity current = identities.get(i);
        whereExpression.equals(JCRProperties.id, current.getProfile().getId());
        if (i + 1 < identities.size()) {
          whereExpression.or();
        }
      }      
    } else {
      whereExpression.equals(JCRProperties.id, "");
    }
    
    whereExpression.endGroup();
    
  }

  public static String processUsernameSearchPattern(final String userName) {
    String modifiedUserName = userName;
    if (modifiedUserName.length() > 0) {
      modifiedUserName =
          ((EMPTY_STR.equals(modifiedUserName)) || (modifiedUserName.length() == 0))
              ? ASTERISK_STR
              : modifiedUserName;

      modifiedUserName =
          (modifiedUserName.charAt(0) != ASTERISK_CHAR) ? ASTERISK_STR + modifiedUserName : modifiedUserName;

      modifiedUserName =
          (modifiedUserName.charAt(modifiedUserName.length() - 1) != ASTERISK_CHAR)
              ? modifiedUserName += ASTERISK_STR
              : modifiedUserName;

      modifiedUserName =
          (modifiedUserName.indexOf(ASTERISK_STR) >= 0)
              ? modifiedUserName.replace(ASTERISK_STR, "." + ASTERISK_STR)
              : modifiedUserName;

      modifiedUserName =
          (modifiedUserName.indexOf(PERCENT_STR) >= 0)
              ? modifiedUserName.replace(PERCENT_STR, "." + ASTERISK_STR)
              : modifiedUserName;

      Pattern.compile(modifiedUserName);
    }
    return userName;
  }

  public static String addAsteriskToStringInput(final String input) {
    if (input.length() != 0) {
      if (input.indexOf(ASTERISK_STR) == -1) {
        return ASTERISK_STR + input + ASTERISK_STR;
      }
      return input;
    }
    return EMPTY_STR;
  }

  /**
   * Encodes Url to conform to the generated Url of WEBDAV.
   * Currently, Could not load data from generated url that contain dot character (.) cause by not consist with WEBDAV.
   * This method replace any percent character (%) by (%25) to solve this problem. 
   * @param path
   * @return
   */
  public static String encodeUrl(String path) {
    PortalContainer container = PortalContainer.getInstance();
    ChromatticManager manager = (ChromatticManager) container.getComponentInstanceOfType(ChromatticManager.class);
    SocialChromatticLifeCycle lifeCycle = (SocialChromatticLifeCycle)
                                          manager.getLifeCycle(SocialChromatticLifeCycle.SOCIAL_LIFECYCLE_NAME);
    ChromatticSession chromatticSession = lifeCycle.getSession();
    StringBuilder encodedUrl = new StringBuilder(); 
    encodedUrl = encodedUrl.append("/").append(container.getRestContextName()).append("/jcr/").
                              append(lifeCycle.getRepositoryName()).append("/").
                              append(chromatticSession.getJCRSession().getWorkspace().getName()).
                              append(path.replaceAll("%", "%25"));
    return encodedUrl.toString();
  }

  /**
   * Process Unified Search Condition
   * @param searchCondition the input search condition
   * @return List of conditions
   * @since 4.0.x
   */
  public static List<String> processUnifiedSearchCondition(String searchCondition) {
    String[] spaceConditions = searchCondition.split(" ");
    List<String> result = new ArrayList<String>(spaceConditions.length);

    for (String conditionValue : spaceConditions) {
      result.add(conditionValue);
    }
    return result;
  }
  
  /**
   * Gets common item number from two list
   * @param m the first list
   * @param n the second list
   * @return number of common item
   */
  public static <T> int getCommonItemNumber(final List<T> m, final List<T> n) {
    if (m == null || n == null) {
      return 0;
    }
    List<T> copy = new ArrayList<T>(m);
    copy.removeAll(n);
    
    return (m.size() - copy.size());
  }
  
  /**
   * Sort one map by its value
   * @param map the input map
   * @param asc indicate sort by ASC (true) or DESC (false)
   * @return the sorted map
   * @since 4.0.x
   */
  public static <K, V extends Comparable<? super V>> Map<K, V> sortMapByValue( Map<K, V> map , final boolean asc) {
    //
    List<Map.Entry<K, V>> list = new LinkedList<Map.Entry<K, V>>( map.entrySet() );
    //
    Collections.sort( list, new Comparator<Map.Entry<K, V>>() {
      public int compare( Map.Entry<K, V> o1, Map.Entry<K, V> o2 ) {
        if (asc)
          return (o1.getValue()).compareTo( o2.getValue() );
        else
          return (o1.getValue()).compareTo( o2.getValue() )/-1;
      }
    });

    Map<K, V> result = new LinkedHashMap<K, V>();
    for (Map.Entry<K, V> entry : list) {
      result.put(entry.getKey(), entry.getValue());
    }
    return result;
  }
  
  /**
   * Sort list of spaces by space's display name
   * 
   * @param list
   * @param asc
   * @return sorted list
   */
  public static List<Space> sortSpaceByName(List<Space> list, final boolean asc) {
    //
    Collections.sort(list, new Comparator<Space>() {
      public int compare(Space o1, Space o2) {
        if (asc)
          return (o1.getDisplayName()).compareTo(o2.getDisplayName());
        else
          return (o1.getDisplayName()).compareTo(o2.getDisplayName()) / -1;
      }
    });

    return list;
  }
  
  /**
   * Sort list of identities by full name
   * 
   * @param list
   * @param asc
   * @return sorted list
   */
  public static List<Identity> sortIdentitiesByFullName(List<Identity> list, final boolean asc) {
    //
    Collections.sort(list, new Comparator<Identity>() {
      public int compare(Identity o1, Identity o2) {
        if (asc)
          return (o1.getProfile().getFullName()).compareTo(o2.getProfile().getFullName());
        else
          return (o1.getProfile().getFullName()).compareTo(o2.getProfile().getFullName()) / -1;
      }
    });

    return list;
  }
  
  /**
   * Sort a list of activity by updated time
   * 
   * @param list
   * @return
   */
  public static List<ExoSocialActivity> sortActivitiesByTime(List<ExoSocialActivity> list, int limit) {
    //
    Collections.sort(list, new Comparator<ExoSocialActivity>() {
      public int compare(ExoSocialActivity a1, ExoSocialActivity a2) {
        return ((Long)a1.getUpdated().getTime()).compareTo((Long)a2.getUpdated().getTime()) / -1;
      }
    });

    return list.size() > limit ? list.subList(0, limit - 1) : list;
  }
  
  /**
   * Gets sub list from the provided list with start and end index.
   * @param list the identity list
   * @param startIndex start index to get
   * @param toIndex end index to get
   * @return sub list of the provided list
   */
  public static <T> List<T> subList(List<T> list, int startIndex, int toIndex) {
    int totalSize = list.size();
    
    if (startIndex >= totalSize) return Collections.emptyList();
    
    //
    if ( toIndex >= totalSize ) {
      toIndex = totalSize;
    }
    
    return list.subList(startIndex, toIndex);
  }
  /*
   * Gets added element when compares between l1 and l2
   * @param l1
   * @param l2
   * @return
   */
  public static String[] sub(String[] l1, String[] l2) {

    if (l1 == null) {
      return new String[]{};
    }

    if (l2 == null) {
      return l1;
    }

    List<String> l = new ArrayList(Arrays.asList(l1));
    l.removeAll(Arrays.asList(l2));
    return l.toArray(new String[]{});
  }
  /**
   * Make the decision to persist JCR Storage or not
   * @return
   */
  public static boolean persist() {
    try {
      ChromatticSession chromatticSession = AbstractStorage.lifecycleLookup().getSession();
      chromatticSession.save();
    } catch (Exception e) {
      return false;
    }
    return true;
  }
  
  /**
   * Make the decision to persist JCR Storage and refresh session or not
   * 
   * @return
   */
  public static boolean persist(boolean isRefresh) {
    try {
      ChromatticSession chromatticSession = AbstractStorage.lifecycleLookup().getSession();
      if (chromatticSession.getJCRSession().hasPendingChanges()) {
        chromatticSession.getJCRSession().save();
        if (isRefresh) {
          chromatticSession.getJCRSession().refresh(true);
        }

      }
    } catch (Exception e) {
      return false;
    }
    return true;
  }
  
  /**
   * Make the decision to persist JCR Storage or not
   * @return
   */
  public static boolean persistJCR(boolean beginRequest) {
    try {
      //push to JCR
      AbstractStorage.lifecycleLookup().closeContext(true);
      
      if (beginRequest) {
        AbstractStorage.lifecycleLookup().openContext();
      }
      
    } catch (Exception e) {
      return false;
    }
    return true;
  }
  
  /**
   * End the request for ChromatticManager 
   * @return
   */
  public static boolean endRequest() {
    try {
      
      ExoContainer container = ExoContainerContext.getCurrentContainer();
      ChromatticManager manager = (ChromatticManager) container.getComponentInstanceOfType(ChromatticManager.class);
      Synchronization synchronization = manager.getSynchronization();
      if (synchronization != null) {
        synchronization.setSaveOnClose(true);
        //close synchronous and session.logout
        manager.endRequest(true);
      }
    } catch (Exception e) {
      return false;
    }
    return true;
  }
  
  /**
   * Returns a collection containing all the elements in <code>list1</code> that
   * are also in <code>list2</code>.
   * 
   * @param list1
   * @param list2
   * @return
   */
  public <T> List<T> intersection(List<T> list1, List<T> list2) {
    List<T> list = new ArrayList<T>();

    for (T t : list1) {
      if (list2.contains(t)) {
        list.add(t);
      }
    }

    return list;
  }
  /**
   * Returns a array containing all the elements in <code>list1</code> that
   * @param array1
   * @param array2
   * @return
   */
  public <T> T[] intersection(T[] array1, T[] array2) {
    List<T> got = intersection(Arrays.asList(array1), Arrays.asList(array2));
    return (T[]) got.toArray();
  }
  
  /**
   * Returns a new {@link List} containing a - b
   * @param a
   * @param b
   * @return
   */
  public static <T> List<T> sub(final Collection<T> a, final Collection<T> b) {
    ArrayList<T> list = new ArrayList<T>(a);
    for (Iterator<T> it = b.iterator(); it.hasNext();) {
        list.remove(it.next());
    }
    return list;
  }

  /**
   * Retrieves the user list who has last login around given days.
   * @param aroundDays the given days.
   * @return The list of users.
   */
  public static Set<String> getLastLogin(int aroundDays) {
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.DAY_OF_MONTH, 0 - aroundDays);
    long fromDay = calendar.getTimeInMillis();
    try {
      if (cls != null) {
        Class<?>[] params = new Class<?>[1];
        params[0] = Long.TYPE;
        Method method = cls.getMethod("getLastUsersLogin", params);
        Object obj = CommonsUtils.getService(cls);
        return (Set<String>) method.invoke(obj, fromDay);
      } else {
        return null;
      }
    } catch (Exception e) {
      LOG.warn("Failed to invoke method " + e.getMessage());
      return null;
    }
  }

  /**
   * 
   * @param aroundDays
   * @param lazilyCreatedTime
   * @return
   */
  public static boolean isActiveUser(int aroundDays, long lazilyCreatedTime) {
    Calendar cal = Calendar.getInstance();
    cal.set(Calendar.DAY_OF_MONTH, cal.get(Calendar.DAY_OF_MONTH) - aroundDays);
    long limitTime = cal.getTimeInMillis(); 
    return lazilyCreatedTime >= limitTime;
  }
  
  public static long getBeforeLastLogin(String userId) {
    try {
      if (cls != null) {
        Class<?>[] params = new Class<?>[1];
        params[0] = String.class;
        Method method = cls.getMethod("getBeforeLastLogin", params);
        Object obj = CommonsUtils.getService(cls);
        return (Long) method.invoke(obj, userId);
      } else {
        return 0;
      }
    } catch (Exception e) {
      LOG.error("Failed to invoke method " + e.getMessage(), e);
      return 0;
    }
  }
  
  public static Map<String, Integer> getActiveUsers(int aroundDays) {
    try {
      if (cls != null) {
        Class<?>[] params = new Class<?>[1];
        params[0] = Integer.TYPE;
        Method method = cls.getMethod("getActiveUsers", params);
        Object obj = CommonsUtils.getService(cls);
        return (Map<String, Integer>) method.invoke(obj, aroundDays);
      } else {
        return new HashMap<String, Integer>();
      }
      
    } catch (Exception e) {
      LOG.error("Failed to invoke method " + e.getMessage(), e);
      return null;
    }
  }
  
  /**
  * Compares oldDate and newDate.
  *
  * return TRUE if given newDate the after one day or more the given oldDate
  * @param oldDate
  * @param newDate
  * @return TRUE: the day after oldDate
  */
    public static boolean afterDayOrMore(long oldDate, long newDate) {
      long diffValue = newDate - oldDate;
      return diffValue >= DAY_MILISECONDS;
    }
    
  /**
   * Gets the list of activity's id from the activities  
   * @param activities
   * @return list of ids
   */
  public static List<String> getIds(List<ExoSocialActivity> activities) {
    List<String> ids = new LinkedList<String>();
    for (ExoSocialActivity a : activities) {
      ids.add(a.getId());
    }

    return ids;
  }
}
