/*
 * Copyright (C) 2012 Issa Gorissen <issa-gorissen@usa.net>. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.openfire.crowd;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.group.AbstractGroupProvider;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;


/**
 * Atlassian Crowd implementation of the GroupProvider. We do not permit
 * modifications of groups from this provider - only read-only access.
 */
public class CrowdGroupProvider extends AbstractGroupProvider {
	private static final Logger LOG = LoggerFactory.getLogger(CrowdGroupProvider.class);
	private static final int CACHE_TTL = 3600; // ttl in seconds - one hour
	private static final String JIVE_CROWD_GROUPS_CACHE_TTL_SECS = "crowd.groups.cache.ttl.seconds";
	
	private static final String GROUP_CACHE_NAME = "crowdGroup";
	private static final String GROUP_MEMBERSHIP_CACHE_NAME = "crowdGroupMembership";
	private static final String USER_MEMBERSHIP_CACHE_NAME = "crowdUserMembership";

	private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	private static final ScheduledExecutorService crowdGroupSync = Executors.newSingleThreadScheduledExecutor();
	private static final CrowdManager manager = CrowdManager.getInstance();

	private static List<String> groups = new ArrayList<>();
	
	private final XMPPServer server = XMPPServer.getInstance();
	
	
	static {
		String propertyValue = JiveGlobals.getProperty(JIVE_CROWD_GROUPS_CACHE_TTL_SECS);
		int ttl = (propertyValue == null || propertyValue.trim().length() == 0) ? CACHE_TTL : Integer.parseInt(propertyValue);

		crowdGroupSync.scheduleAtFixedRate(new GroupSynch(), 0, ttl, TimeUnit.SECONDS);
		
		JiveGlobals.setProperty(JIVE_CROWD_GROUPS_CACHE_TTL_SECS, String.valueOf(ttl));
	}
	
	public CrowdGroupProvider() {
		String propertyValue = JiveGlobals.getProperty(JIVE_CROWD_GROUPS_CACHE_TTL_SECS);
		int ttl = (propertyValue == null || propertyValue.trim().length() == 0) ? CACHE_TTL : Integer.parseInt(propertyValue);

		Cache<String, Collection<JID>> groupMembershipCache = CacheFactory.createLocalCache(GROUP_MEMBERSHIP_CACHE_NAME);
		groupMembershipCache.setMaxCacheSize(-1);
		groupMembershipCache.setMaxLifetime(ttl * 1000); // msecs instead of sec - see Cache API

		Cache<JID, Collection<String>> userMembershipCache = CacheFactory.createLocalCache(USER_MEMBERSHIP_CACHE_NAME);
		userMembershipCache.setMaxCacheSize(-1);
		userMembershipCache.setMaxLifetime(ttl * 1000); // msecs instead of sec - see Cache API
		
		Cache<String, org.jivesoftware.openfire.crowd.jaxb.Group> groupCache = CacheFactory.createLocalCache(GROUP_CACHE_NAME);
		groupCache.setMaxCacheSize(-1);
		groupCache.setMaxLifetime(ttl * 1000); // msecs instead of sec - see Cache API
	}

	@Override
	public Group getGroup(String name) throws GroupNotFoundException {
		try {
			Cache<String, org.jivesoftware.openfire.crowd.jaxb.Group> groupCache = CacheFactory.createLocalCache(GROUP_CACHE_NAME);
			org.jivesoftware.openfire.crowd.jaxb.Group group = groupCache.get(name);
			if (group == null) {
				group = manager.getGroup(name);
				groupCache.put(name, group);
			}
			Collection<JID> members = getGroupMembers(name);
			Collection<JID> admins = Collections.emptyList();
			return new Group(name, group.description, members, admins);
			
		} catch (RemoteException re) {
			LOG.error("Failure to load group:" + String.valueOf(name), re);
			throw new GroupNotFoundException(re);
		}
	}

	
	private Collection<JID> getGroupMembers(String groupName) {
		Cache<String, Collection<JID>> groupMembershipCache = CacheFactory.createLocalCache(GROUP_MEMBERSHIP_CACHE_NAME);
		Collection<JID> members = groupMembershipCache.get(groupName);
		if (members != null) {
			return members;
		}
		
		try {
			List<String> users = manager.getGroupMembers(groupName);
			Collection<JID> results = new ArrayList<>();

			for (String username : users) {
				results.add(server.createJID(username, null));
			}
			
			groupMembershipCache.put(groupName, results);
			return results;
			
		} catch (RemoteException re) {
			LOG.error("Failure to get the members of crowd group:" + String.valueOf(groupName), re);
		}
		
		groupMembershipCache.put(groupName, new ArrayList<JID>());
		return Collections.emptyList();
	}
	
	@Override
	public Collection<String> getGroupNames(JID user) {
		Cache<JID, Collection<String>> userMembershipCache = CacheFactory.createCache(USER_MEMBERSHIP_CACHE_NAME);
		Collection<String> groups = userMembershipCache.get(user);
		if (groups != null) {
			return groups;
		}
		
		try {
			groups = manager.getUserGroups(user.getNode());
			userMembershipCache.put(user, groups);
			return groups;
		} catch (RemoteException re) {
			LOG.error("Failure to load the groups of user:" + String.valueOf(user), re);
		}
		
		userMembershipCache.put(user, new ArrayList<String>());
		return Collections.emptyList();
	}

	@Override
	public int getGroupCount() {
		lock.readLock().lock();
		try {
			return groups.size();
		} finally {
			lock.readLock().unlock();
		}
	}

	@Override
	public Collection<String> getGroupNames() {
		lock.readLock().lock();
		try {
			return groups;
		} finally {
			lock.readLock().unlock();
		}
	}

	@Override
	public Collection<String> getGroupNames(int startIndex, int numResults) {
		lock.readLock().lock();
		try {
			Collection<String> results = new ArrayList<>(numResults);
			
			for (int i = 0, j = startIndex; i < numResults && j < groups.size(); ++i, ++j) {
				results.add(groups.get(j));
			}
			
			return results;
		} finally {
			lock.readLock().unlock();
		}
	}

	@Override
	public Collection<String> search(String query) {
		lock.readLock().lock();
		try {
			ArrayList<String> results = new ArrayList<>();
			
			if (query != null && query.trim().length() > 0) {
				
				if (query.endsWith("*")) {
					query = query.substring(0, query.length() - 1);
				}
				if (query.startsWith("*")) {
					query = query.substring(1);
				}
				query = query.toLowerCase();
				
				for (String groupName : groups) {
					if (groupName.toLowerCase().contains(query)) {
						results.add(groupName);
					}
				}
			}
			
			return results;
			
		} finally {
			lock.readLock().unlock();
		}
	}

	@Override
	public Collection<String> search(String query, int startIndex, int numResults) {
		lock.readLock().lock();
		try {
			ArrayList<String> foundGroups = (ArrayList<String>) search(query);
			
			Collection<String> results = new ArrayList<>();
			
			for (int i = 0, j = startIndex; i < numResults && j < foundGroups.size(); ++i, ++j) {
				results.add(foundGroups.get(j));
			}
			
			return results;
			
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * Modifying group not implemented - read-only for now
	 */
	@Override
	public boolean isReadOnly() {
		return true;
	}

	@Override
	public boolean isSearchSupported() {
		return true;
	}

	
	
	/**
	 * @see org.jivesoftware.openfire.group.AbstractGroupProvider#search(java.lang.String, java.lang.String)
	 */
	// TODO search on group attributes in Crowd
	@Override
	public Collection<String> search(String key, String value) {
		LOG.info("Search groups on attibutes not implemented yet");
		return Collections.emptyList();
	}





	static class GroupSynch implements Runnable {
		@Override
		public void run() {
			LOG.info("running synch with crowd...");
			CrowdManager manager = null;
			try {
				manager = CrowdManager.getInstance();
			} catch (Exception e) {
				LOG.error("Failure to load the Crowd manager", e);
				return;
			}
			
			List<String> allGroups = null;
			try {
				allGroups = manager.getAllGroupNames();
			} catch (RemoteException re) {
				LOG.error("Failure to fetch all crowd groups name", re);
				return;
			}
			
			if (allGroups != null && allGroups.size() > 0) {
				CrowdGroupProvider.lock.writeLock().lock();
				try {
					CrowdGroupProvider.groups = allGroups;
				} finally {
					CrowdGroupProvider.lock.writeLock().unlock();
				}
			}
			
			LOG.info("crowd synch done, returned " + allGroups.size() + " groups");
		}
	}

}
