/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.apache.vysper.xmpp.modules.extension.xep0045_muc.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.vysper.xmpp.addressing.Entity;
import org.apache.vysper.xmpp.addressing.EntityImpl;
import org.apache.vysper.xmpp.modules.servicediscovery.management.Feature;
import org.apache.vysper.xmpp.modules.servicediscovery.management.Identity;
import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoElement;
import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoRequest;
import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoRequestListener;
import org.apache.vysper.xmpp.modules.servicediscovery.management.Item;
import org.apache.vysper.xmpp.modules.servicediscovery.management.ItemRequestListener;
import org.apache.vysper.xmpp.modules.servicediscovery.management.ServiceDiscoveryRequestException;
import org.apache.vysper.xmpp.protocol.NamespaceURIs;

/**
 * A chat room
 *
 * @author The Apache MINA Project (dev@mina.apache.org)
 */
public class Room implements InfoRequestListener, ItemRequestListener {

    private EnumSet<RoomType> roomTypes;

    private Entity jid;

    private String name;

    private String password;
    
    private boolean rewriteDuplicateNick = true;

    private DiscussionHistory history = new DiscussionHistory();

    private Affiliations affiliations = new Affiliations();

    // keep in a map to allow for quick access
    private Map<Entity, Occupant> occupants = new ConcurrentHashMap<Entity, Occupant>();

    public Room(Entity jid, String name, RoomType... types) {
        if (jid == null) {
            throw new IllegalArgumentException("JID can not be null");
        } else if (jid.getResource() != null) {
            throw new IllegalArgumentException("JID must be bare");
        }
        if (name == null || name.trim().length() == 0) {
            throw new IllegalArgumentException("Name can not be null or empty");
        }

        this.jid = jid;
        this.name = name;

        EnumSet<RoomType> potentialTypes;
        if (types != null && types.length > 0) {
            potentialTypes = EnumSet.copyOf(Arrays.asList(types));

            // make sure the list does not contain antonyms
            RoomType.validateAntonyms(potentialTypes);
        } else {
            potentialTypes = EnumSet.noneOf(RoomType.class);
        }

        // complement with default types
        this.roomTypes = RoomType.complement(potentialTypes);
    }

    public Entity getJID() {
        return jid;
    }

    public String getName() {
        return name;
    }

    public EnumSet<RoomType> getRoomTypes() {
        return roomTypes.clone();
    }

    public boolean isRoomType(RoomType type) {
        return roomTypes.contains(type);
    }

    public boolean rewritesDuplicateNick() {
        return rewriteDuplicateNick;
    }

    public void setRewriteDuplicateNick(boolean rewriteDuplicateNick) {
        this.rewriteDuplicateNick = rewriteDuplicateNick;
    }

    public Occupant addOccupant(Entity occupantJid, String name) {
        Affiliation affiliation = affiliations.getAffiliation(occupantJid);

        // TODO throw a domain specific exception
        if (affiliation == Affiliation.Outcast) {
            throw new RuntimeException("forbidden");
        }

        // default to none
        if (affiliation == null) {
            affiliation = Affiliation.None;
        }
        
        Role role = Role.getRole(affiliation, roomTypes);
        Occupant occupant = new Occupant(occupantJid, name, this, role);
        if (isRoomType(RoomType.MembersOnly) && affiliation == Affiliation.None) {
            // don't add non member to room
            throw new RuntimeException("registration-required");
        } else {
            occupants.put(occupantJid, occupant);
        }
        return occupant;
    }

    public Occupant findOccupantByJID(Entity occupantJid) {
        return occupants.get(occupantJid);
    }

    public Occupant findOccupantByNick(String nick) {
        for (Occupant occupant : getOccupants()) {
            if (occupant.getNick().equals(nick))
                return occupant;
        }

        return null;
    }

    public Set<Occupant> getModerators() {
        return getByRole(Role.Moderator);
    }
    
    private Set<Occupant> getByRole(Role role) {
        Set<Occupant> matches = new HashSet<Occupant>();
        for (Occupant occupant : getOccupants()) {
            if (role.equals(occupant.getRole()))
                matches.add(occupant);
        }
        return matches;
    }
    
    public boolean isInRoom(Entity jid) {
        return findOccupantByJID(jid) != null;
    }

    public boolean isInRoom(String nick) {
        return findOccupantByNick(nick) != null;
    }

    public void removeOccupant(Entity occupantJid) {
        occupants.remove(occupantJid);
    }

    public int getOccupantCount() {
        return occupants.size();
    }

    public boolean isEmpty() {
        return occupants.isEmpty();
    }

    public Set<Occupant> getOccupants() {
        Set<Occupant> set = new HashSet<Occupant>(occupants.values());
        return Collections.unmodifiableSet(set);
    }

    public List<InfoElement> getInfosFor(InfoRequest request) throws ServiceDiscoveryRequestException {
        List<InfoElement> infoElements = new ArrayList<InfoElement>();
        infoElements.add(new Identity("conference", "text", getName()));
        infoElements.add(new Feature(NamespaceURIs.XEP0045_MUC));

        for (RoomType type : roomTypes) {
            if (type.includeInDisco()) {
                infoElements.add(new Feature(type.getDiscoName()));
            }
        }

        return infoElements;
    }

    public List<Item> getItemsFor(InfoRequest request) throws ServiceDiscoveryRequestException {
        // List of users
        List<Item> items = new ArrayList<Item>();

        // TODO is this the right way to determine if the room is private?
        if (isRoomType(RoomType.FullyAnonymous) || isRoomType(RoomType.SemiAnonymous)) {
            // private room, return empty list
        } else {
            for (Occupant occupant : getOccupants()) {
                items.add(new Item(new EntityImpl(getJID(), occupant.getNick())));
            }
        }
        return items;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public DiscussionHistory getHistory() {
        return history;
    }

    public Affiliations getAffiliations() {
        return affiliations;
    }

}
