/* BdfServer - Copyright (c) 2013-2017 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package fr.exemole.bdfserver.impl;

import fr.exemole.bdfserver.api.BdfServerEditor;
import fr.exemole.bdfserver.api.managers.PermissionManager;
import fr.exemole.bdfserver.api.roles.Role;
import fr.exemole.bdfserver.api.roles.RoleDef;
import fr.exemole.bdfserver.api.storage.RedacteurListStorage;
import fr.exemole.bdfserver.api.storage.RoleStorage;
import fr.exemole.bdfserver.api.storage.StorageCheck;
import fr.exemole.bdfserver.tools.roles.RoleUtils;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import net.fichotheque.ExistingIdException;
import net.fichotheque.Fichotheque;
import net.fichotheque.FichothequeConstants;
import net.fichotheque.sphere.Redacteur;
import net.fichotheque.utils.SphereUtils;
import net.mapeadores.util.text.StringUtils;


/**
 *
 * @author Vincent Calame
 */
class PermissionManagerImpl implements PermissionManager {

    private final static List<Redacteur> EMPTY_REDACTEURLIST = Collections.emptyList();
    private final GlobalIdSet adminSet = new GlobalIdSet();
    private final Fichotheque fichotheque;
    private final RoleStorage roleStorage;
    private final RedacteurListStorage redacteurListStorage;
    private Map<String, RoleImpl> roleMap;
    private Roles globalRoles;
    private final Map<String, GlobalIdSet> redacteurByRoleMap = new HashMap<String, GlobalIdSet>();
    private final Map<String, Roles> roleByRedacteurMap = new HashMap<String, Roles>();
    private List<Role> defaultSingletonRoleList;

    private PermissionManagerImpl(Fichotheque fichotheque, RoleStorage roleStorage, RedacteurListStorage redacteurListStorage) {
        this.fichotheque = fichotheque;
        this.roleStorage = roleStorage;
        this.redacteurListStorage = redacteurListStorage;
    }

    static PermissionManagerImpl build(Fichotheque fichotheque, RoleStorage roleStorage, RedacteurListStorage redacteurListStorage, StorageCheck storageCheck) {
        PermissionManagerImpl impl = new PermissionManagerImpl(fichotheque, roleStorage, redacteurListStorage);
        SortedMap<String, RoleImpl> roleMap = new TreeMap<String, RoleImpl>();
        RoleImpl defaultRole = null;
        for (RoleDef roleDef : storageCheck.getRoleDefList()) {
            String name = roleDef.getName();
            if (roleMap.containsKey(name)) {
                continue;
            }
            RoleImpl roleImpl = RoleImpl.fromRoleDef(roleDef);
            if (name.equals(Role.DEFAULT_ROLE)) {
                defaultRole = roleImpl;
            }
            roleMap.put(name, roleImpl);
        }
        if (defaultRole == null) {
            defaultRole = RoleImpl.fromName(Role.DEFAULT_ROLE);
            roleMap.put(Role.DEFAULT_ROLE, defaultRole);
        }
        impl.roleMap = roleMap;
        impl.globalRoles = new Roles(new ArrayList<RoleImpl>(roleMap.values()));
        impl.defaultSingletonRoleList = Collections.singletonList((Role) defaultRole);
        for (Map.Entry<String, List<Redacteur>> entry : storageCheck.getRedacteurListMap().entrySet()) {
            String name = entry.getKey();
            List<Redacteur> redacteurList = entry.getValue();
            if (name.equals("admin")) {
                for (Redacteur redacteur : redacteurList) {
                    impl.setAdmin(redacteur.getGlobalId(), true);
                }
            } else if (name.startsWith("role_")) {
                String roleName = name.substring(5);
                Role role = impl.getRole(roleName);
                if (role != null) {
                    for (Redacteur redacteur : redacteurList) {
                        impl.link(redacteur, role);
                    }
                }
            }
        }
        return impl;
    }

    @Override
    public boolean isAdmin(Redacteur redacteur) {
        return adminSet.containsGlobalId(redacteur.getGlobalId());
    }

    @Override
    public List<Redacteur> getAdminRedacteurList() {
        return adminSet.toRedacteurList(fichotheque);
    }

    @Override
    public List<Role> getRoleList() {
        return globalRoles.getRoleList();
    }

    @Override
    public List<Role> getRoleList(Redacteur redacteur) {
        Roles roles = roleByRedacteurMap.get(redacteur.getGlobalId());
        if ((roles != null) && (roles.size() > 0)) {
            return roles.getRoleList();
        } else {
            return defaultSingletonRoleList;
        }
    }

    @Override
    public List<Redacteur> getRedacteurList(Role role) {
        GlobalIdSet globalIdSet = redacteurByRoleMap.get(role.getName());
        if (globalIdSet != null) {
            return globalIdSet.toRedacteurList(fichotheque);
        } else {
            return EMPTY_REDACTEURLIST;
        }
    }

    @Override
    public Role getRole(String roleName) {
        return roleMap.get(roleName);
    }

    void redacteurRemoved(BdfServerEditor bdfServerEditor, String globalId) {
        Roles roles = roleByRedacteurMap.get(globalId);
        if (roles != null) {
            for (Role role : roles.getRoleList()) {
                bdfServerEditor.unlink(globalId, role);
            }
            roleByRedacteurMap.remove(globalId);
        }
        if (adminSet.containsGlobalId(globalId)) {
            if (adminSet.size() == 1) {
                throw new UnsupportedOperationException("Last admin cannot be removed");
            }
            bdfServerEditor.setAdmin(globalId, false);
        }
    }

    boolean setAdmin(String globalId, boolean isAdmin) {
        if (isAdmin) {
            if (adminSet.containsGlobalId(globalId)) {
                return false;
            } else {
                Redacteur redacteur = SphereUtils.getRedacteur(fichotheque, globalId);
                if ((redacteur == null) || (!redacteur.getStatus().equals(FichothequeConstants.ACTIVE_STATUS))) {
                    return false;
                } else {
                    adminSet.addGlobalId(globalId);
                    return true;
                }
            }
        } else {
            if (adminSet.containsGlobalId(globalId)) {
                if (adminSet.size() == 1) {
                    return false;
                }
                adminSet.removeGlobalId(globalId);
                return true;
            } else {
                return false;
            }
        }
    }

    boolean link(Redacteur redacteur, Role role) {
        getOrCreateRoleList(redacteur.getGlobalId()).addRole((RoleImpl) role);
        boolean done = getOrCreateGlobalIdSet(role.getName()).addGlobalId(redacteur.getGlobalId());
        return done;
    }

    boolean unlink(String globalId, Role role) {
        RoleImpl roleImpl = (RoleImpl) role;
        String roleName = role.getName();
        Roles roles = roleByRedacteurMap.get(globalId);
        if (roles != null) {
            roles.removeRole(roleImpl);
            if (roles.size() == 0) {
                roleByRedacteurMap.remove(globalId);
            }
        }
        boolean done = false;
        GlobalIdSet globalIdSet = redacteurByRoleMap.get(roleName);
        if (globalIdSet != null) {
            done = globalIdSet.removeGlobalId(globalId);
            if (globalIdSet.size() == 0) {
                redacteurByRoleMap.remove(roleName);
            }
        }
        return done;
    }

    RoleImpl createRole(String roleName) throws ParseException, ExistingIdException {
        StringUtils.checkTechnicalName(roleName, true);
        if (roleMap.containsKey(roleName)) {
            throw new ExistingIdException();
        }
        RoleImpl roleImpl = RoleImpl.fromName(roleName);
        roleMap.put(roleName, roleImpl);
        globalRoles.addRole(roleImpl);
        return roleImpl;
    }

    boolean prepareRoleToRemove(String roleName) {
        if (roleName.equals(Role.DEFAULT_ROLE)) {
            throw new IllegalArgumentException("Default role cannot be removed");
        }
        if (!roleMap.containsKey(roleName)) {
            return false;
        }
        RoleImpl roleImpl = roleMap.remove(roleName);
        globalRoles.removeRole(roleImpl);
        GlobalIdSet globalIdSet = redacteurByRoleMap.get(roleName);
        if (globalIdSet != null) {
            for (String globalId : globalIdSet.set) {
                Roles roles = roleByRedacteurMap.get(globalId);
                if (roles != null) {
                    roles.removeRole(roleImpl);
                    if (roles.size() == 0) {
                        roleByRedacteurMap.remove(globalId);
                    }
                }
            }
            redacteurByRoleMap.remove(roleName);
        }
        return true;
    }

    RoleStorage getRoleStorage() {
        return roleStorage;
    }

    RedacteurListStorage getRedacteurListStorage() {
        return redacteurListStorage;
    }

    GlobalIdSet getOrCreateGlobalIdSet(String roleName) {
        GlobalIdSet globalIdSet = redacteurByRoleMap.get(roleName);
        if (globalIdSet == null) {
            globalIdSet = new GlobalIdSet();
            redacteurByRoleMap.put(roleName, globalIdSet);
        }
        return globalIdSet;
    }

    Roles getOrCreateRoleList(String redacteurGlobalId) {
        Roles roleList = roleByRedacteurMap.get(redacteurGlobalId);
        if (roleList == null) {
            roleList = new Roles(new ArrayList<RoleImpl>());
            roleByRedacteurMap.put(redacteurGlobalId, roleList);
        }
        return roleList;
    }

    void saveRedacteurList(String name) {
        List<Redacteur> redacteurList = null;
        if (name.equals("admin")) {
            redacteurList = getAdminRedacteurList();
        } else if (name.startsWith("role_")) {
            String roleName = name.substring(5);
            Role role = getRole(roleName);
            if (role != null) {
                redacteurList = getRedacteurList(role);
            }
        }
        if (redacteurList != null) {
            redacteurListStorage.saveRedacteurList(name, redacteurList);
        }
    }

    void saveRole(RoleImpl roleImpl) {
        roleStorage.saveRoleDef(roleImpl);
    }

    void removeRole(String roleName) {
        roleStorage.removeRoleDef(roleName);
        redacteurListStorage.removeRedacteurList("role_" + roleName);
    }


    private static class Roles {

        private final List<RoleImpl> list;
        private List<Role> cacheList;

        private Roles(List<RoleImpl> list) {
            this.list = list;
        }

        private List<Role> getRoleList() {
            List<Role> result = cacheList;
            if (result == null) {
                result = initRoleList();
            }
            return result;
        }

        private synchronized List<Role> initRoleList() {
            if (cacheList != null) {
                return cacheList;
            }
            List<Role> newList = RoleUtils.wrap(list.toArray(new Role[list.size()]));
            cacheList = newList;
            return newList;
        }

        private void clearCache() {
            cacheList = null;
        }

        private int size() {
            return list.size();
        }

        private synchronized boolean addRole(RoleImpl role) {
            clearCache();
            String roleName = role.getName();
            int size = list.size();
            boolean done = false;
            for (int i = 0; i < size; i++) {
                String currentName = list.get(i).getName();
                if (currentName.equals(roleName)) {
                    return false;
                } else if (roleName.compareTo(currentName) < 0) {
                    list.add(i, role);
                    done = true;
                    break;
                }
            }
            if (!done) {
                list.add(role);
            }
            return true;
        }

        private synchronized boolean removeRole(RoleImpl role) {
            clearCache();
            return list.remove(role);
        }

    }


    private static class GlobalIdSet {

        private final SortedSet<String> set = new TreeSet<String>();

        private GlobalIdSet() {
        }

        private int size() {
            return set.size();
        }

        private boolean addGlobalId(String globalId) {
            return set.add(globalId);
        }

        private boolean removeGlobalId(String globalId) {
            return set.remove(globalId);
        }

        private boolean containsGlobalId(String globalId) {
            return set.contains(globalId);
        }

        private List<Redacteur> toRedacteurList(Fichotheque fichotheque) {
            List<Redacteur> redacteurList = new ArrayList<Redacteur>();
            for (String redacteurGlobalId : set) {
                Redacteur redacteur = SphereUtils.getRedacteur(fichotheque, redacteurGlobalId);
                if (redacteur != null) {
                    redacteurList.add(redacteur);
                }
            }
            return redacteurList;
        }

    }

}
