/*
 * ARX Data Anonymization Tool
 * Copyright 2012 - 2024 Fabian Prasser and contributors
 * 
 * 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.deidentifier.arx.gui.model;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.deidentifier.arx.ARXLattice;
import org.deidentifier.arx.ARXLattice.ARXNode;
import org.deidentifier.arx.ARXLattice.Anonymity;
import org.deidentifier.arx.ARXResult;
import org.deidentifier.arx.DataDefinition;
import org.deidentifier.arx.DataHandle;
import org.deidentifier.arx.gui.resources.Resources;

/**
 * This class implements a filter for a generalization lattice.
 *
 * @author Fabian Prasser
 */
public class ModelNodeFilter implements Serializable {

    /** SVUID. */
    private static final long    serialVersionUID   = 5451641489562102719L;

    /** The anonymity properties allowed. */
    private final Set<Anonymity> anonymity          = new HashSet<Anonymity>();
    
    /** The generalization levels allowed. */
    private Set<Integer>[]       generalizations    = null;
    
    /** The initial number of nodes. */
    private int                  maxNumNodesInitial = 0;
    
    /** Bound for min. score. */
    private double               minInformationLoss = 0d;
    
    /** Bound for max. score. */
    private double               maxInformationLoss = 1d;

    /**
     * Creates a new instance.
     *
     * @param maxLevels
     * @param maxNumNodesInitial
     */
    @SuppressWarnings("unchecked")
    public ModelNodeFilter(final int[] maxLevels, final int maxNumNodesInitial) {
        this.maxNumNodesInitial = maxNumNodesInitial;
        this.generalizations = new Set[maxLevels.length];
        for (int i = 0; i < generalizations.length; i++) {
            generalizations[i] = new HashSet<Integer>();
        }
    }
    
    /**
     * Creates a new instance used for cloning.
     * 
     * @param anonymity
     * @param generalizations
     * @param maxNumNodesInitial
     * @param minInformationLoss
     * @param maxInformationLoss
     */
    @SuppressWarnings("unchecked")
    private ModelNodeFilter(Set<Anonymity> anonymity, Set<Integer>[] generalizations, int maxNumNodesInitial, double minInformationLoss, double maxInformationLoss) {
        for (Anonymity element : anonymity) {
            this.anonymity.add(element);
        }

        this.generalizations = new Set[generalizations.length];
        for (int i = 0; i < generalizations.length; i++) {
            Set<Integer> current = generalizations[i];
            this.generalizations[i] = new HashSet<Integer>();
            for (Integer integer : current) {
                this.generalizations[i].add(integer);
            }
        }

        this.maxNumNodesInitial = maxNumNodesInitial;
        this.minInformationLoss = minInformationLoss;
        this.maxInformationLoss = maxInformationLoss;
    }

    /**
     * Allows transformations with any score to pass the filter.
     */
    public void allowAllInformationLoss() {
        minInformationLoss = 0d;
        maxInformationLoss = 1d;
    }


    /**
     * Allows anonymous transformations to pass the filter.
     */
    public void allowAnonymous() {
        anonymity.add(Anonymity.ANONYMOUS);
    }

    /**
     * Allows transformations with certain generalization level to pass the filter.
     *
     * @param dimension
     * @param level
     */
    public void allowGeneralization(final int dimension, final int level) {
        generalizations[dimension].add(level);
    }

    /**
     * Allows transformations with certain score to pass the filter.
     *
     * @param min
     * @param max
     */
    public void allowInformationLoss(final double min, final double max) {
        if (min<0d || min>1d || max <0d || max>1d) {
            throw new IllegalArgumentException(Resources.getMessage("Model.0a")); //$NON-NLS-1$
        }
        minInformationLoss = min;
        maxInformationLoss = max;
    }

    /**
     * Allows non-anonymous transformations to pass the filter.
     */
    public void allowNonAnonymous() {
        anonymity.add(Anonymity.NOT_ANONYMOUS);
    }

    /**
     * Allows unknown transformations to pass the filter.
     */
    public void allowUnknown() {
        anonymity.add(Anonymity.PROBABLY_NOT_ANONYMOUS);
        anonymity.add(Anonymity.PROBABLY_ANONYMOUS);
        anonymity.add(Anonymity.UNKNOWN);
    }

    /**
     * Clones the ModelNodeFilter instance.
     */
    public ModelNodeFilter clone() {
        return new ModelNodeFilter(anonymity, generalizations, maxNumNodesInitial, minInformationLoss, maxInformationLoss);
    }

    /**
     * Allows no transformaions to pass the filter.
     */
    public void disallowAll() {
        anonymity.clear();
        minInformationLoss = 0d;
        maxInformationLoss = 1d;
        for (int i = 0; i < generalizations.length; i++) {
            generalizations[i].clear();
        }
    }

    /**
     * Filters out anonymous transformations.
     */
    public void disallowAnonymous() {
        anonymity.remove(Anonymity.ANONYMOUS);
    }

    /**
     * Filters out transformations with certain generalization level.
     *
     * @param dimension
     * @param level
     */
    public void disallowGeneralization(final int dimension, final int level) {
        generalizations[dimension].remove(level);
    }

    /**
     * Filters out non-anonymous transformations.
     */
    public void disallowNonAnonymous() {
        anonymity.remove(Anonymity.NOT_ANONYMOUS);
    }

    /**
     * Filters out unknown transformations.
     */
    public void disallowUnknown() {
        anonymity.remove(Anonymity.PROBABLY_ANONYMOUS);
        anonymity.remove(Anonymity.PROBABLY_NOT_ANONYMOUS);
        anonymity.remove(Anonymity.UNKNOWN);
    }

    /**
     * @return the anonymity
     */
    public Set<Anonymity> getAllowedAnonymity() {
        return anonymity;
    }

    /**
     * 
     *
     * @param dimension
     * @return the generalizations
     */
    public Set<Integer> getAllowedGeneralizations(final int dimension) {
        return generalizations[dimension];
    }

    /**
     * @return the maxInformationLoss
     */
    public double getAllowedMaxInformationLoss() {
        return maxInformationLoss;
    }

    /**
     * @return the minInformationLoss
     */
    public double getAllowedMinInformationLoss() {
        return minInformationLoss;
    }

    /**
     * Creates a node filter for the given result.
     *
     * @param result
     * @param localTransformation 
     */
    public void initialize(final ARXResult result, boolean localTransformation) {
        disallowAll();
        if (localTransformation) {
            return;
        }
        if (result.isResultAvailable()) {
            
            // Allow specializations and generalizations of optimum
            allowAnonymous();
            final double min = 0d;
            final double max = 1d;
            
            allowInformationLoss(min, max);
            final int[] optimum = result.getGlobalOptimum().getTransformation();
            for (int i = 0; i < optimum.length; i++) {
                allowGeneralization(i, optimum[i]);
            }

            // Build sets of visible and hidden nodes
            final Set<ARXNode> visible = new HashSet<ARXNode>();
            final Set<ARXNode> hidden = new HashSet<ARXNode>();
            visible.add(result.getGlobalOptimum());
            for (final ARXNode[] level : result.getLattice().getLevels()) {
                for (final ARXNode node : level) {
                    if (node.getAnonymity() == Anonymity.ANONYMOUS) {
                        if (!node.equals(result.getGlobalOptimum())) {
                            hidden.add(node);
                        }
                    }
                }
            }

            // Determine max generalization
            int maxgen = 0;
            for (int i = 0; i < optimum.length; i++) {
                maxgen = Math.max(result.getLattice()
                                        .getTop()
                                        .getTransformation()[i], maxgen);
            }

            // Show less generalized nodes
            for (int j = 1; j <= maxgen; j++) {
                for (int i = 0; i < optimum.length; i++) {
                    final int gen = optimum[i] - j;
                    if (gen >= 0) {
                        allowGeneralization(i, gen);
                        final int current = count(result.getLattice(), visible, hidden);
                        if (current > maxNumNodesInitial) {
                            disallowGeneralization(i, gen);
                            return;
                        }
                    }
                }
            }

            // Show more generalized nodes
            for (int j = 1; j <= maxgen; j++) {
                for (int i = 0; i < optimum.length; i++) {
                    final int gen = optimum[i] + j;
                    if (gen <= result.getLattice().getTop().getTransformation()[i]) {
                        allowGeneralization(i, gen);
                        final int current = count(result.getLattice(), visible, hidden);
                        if (current > maxNumNodesInitial) {
                            disallowGeneralization(i, gen);
                            return;
                        }
                    }
                }
            }

            // Clean up
            clean(result.getLattice(), visible, optimum);
        } else {

            // Allow generalizations of bottom
            allowNonAnonymous();
            final double max = 1d;
            final double min = 0d;
            allowInformationLoss(min, max);
            final int[] base = result.getLattice()
                                     .getBottom()
                                     .getTransformation();
            for (int i = 0; i < base.length; i++) {
                allowGeneralization(i, base[i]);
            }

            // Build sets of visible and hidden nodes
            final Set<ARXNode> visible = new HashSet<ARXNode>();
            final Set<ARXNode> hidden = new HashSet<ARXNode>();
            visible.add(result.getLattice().getBottom());
            for (final ARXNode[] level : result.getLattice().getLevels()) {
                for (final ARXNode node : level) {
                    if (node.getAnonymity() == Anonymity.NOT_ANONYMOUS) {
                        if (!node.equals(result.getLattice().getBottom())) {
                            hidden.add(node);
                        }
                    }
                }
            }

            // Determine max generalization
            int maxgen = 0;
            for (int i = 0; i < base.length; i++) {
                maxgen = Math.max(result.getLattice()
                                        .getTop()
                                        .getTransformation()[i], maxgen);
            }

            // Show more generalized nodes
            for (int j = 1; j <= maxgen; j++) {
                for (int i = 0; i < base.length; i++) {
                    final int gen = base[i] + j;
                    if (gen <= result.getLattice().getTop().getTransformation()[i]) {
                        allowGeneralization(i, gen);
                        final int current = count(result.getLattice(), visible, hidden);
                        if (current > maxNumNodesInitial) {
                            disallowGeneralization(i, gen);
                            return;
                        }
                    }
                }
            }

            // Clean up
            clean(result.getLattice(), visible, base);
        }
    }

    /**
     * Returns whether the given node is allowed to pass this filter.
     *
     * @param lattice
     * @param node
     * @return
     */
    public boolean isAllowed(final ARXLattice lattice, final ARXNode node) {
        
        double max = node.getHighestScore().relativeTo(lattice.getLowestScore(), lattice.getHighestScore());
        double min = node.getLowestScore().relativeTo(lattice.getLowestScore(), lattice.getHighestScore());
        
        if (max < minInformationLoss) {
            return false;
        } else if (min > maxInformationLoss) {
            return false;
        } else if (!anonymity.contains(node.getAnonymity())) { return false; }
        final int[] transformation = node.getTransformation();
        for (int i = 0; i < transformation.length; i++) {
            if (!generalizations[i].contains(transformation[i])) { return false; }
        }
        return true;
    }

    /**
     * Returns setting.
     *
     * @return
     */
    public boolean isAllowedAnonymous() {
        return anonymity.contains(Anonymity.ANONYMOUS);
    }

    /**
     * Returns setting.
     *
     * @return
     */
    public boolean isAllowedNonAnonymous() {
        return anonymity.contains(Anonymity.NOT_ANONYMOUS);
    }

    /**
     * Returns setting.
     *
     * @return
     */
    public boolean isAllowedUnknown() {
        return anonymity.contains(Anonymity.PROBABLY_ANONYMOUS) ||
        	   anonymity.contains(Anonymity.PROBABLY_NOT_ANONYMOUS) ||
               anonymity.contains(Anonymity.UNKNOWN);
    }

    /**
     * Resets the filter to display everything
     * @param handle
     * @param definition
     */
    public void reset(final DataHandle handle, final DataDefinition definition) {
        
        if (handle == null || definition == null) {
            return;
        }

        List<String> attributes = new ArrayList<String>();
        attributes.addAll(definition.getQuasiIdentifiersWithGeneralization());
        attributes.addAll(definition.getQuasiIdentifiersWithClusteringAndMicroaggregation());
        Collections.sort(attributes, new Comparator<String>(){
            public int compare(String arg0, String arg1) {
                return handle.getColumnIndexOf(arg0)- handle.getColumnIndexOf(arg1);
            }
        });
        

        int dimension=0;
        for (String attribute : attributes) {
            int attributeMin = definition.getMinimumGeneralization(attribute);
            int attributeMax = definition.getMaximumGeneralization(attribute);
            for (int i=attributeMin; i<=attributeMax; i++){
                this.allowGeneralization(dimension, i);
            }
            dimension++;
        }

        this.allowAllInformationLoss();
        this.allowAnonymous();
        this.allowNonAnonymous();
        this.allowUnknown();
    }

    /**
     * Cleans up the settings.
     *
     * @param lattice
     * @param visible
     * @param optimum
     */
    private void clean(final ARXLattice lattice, final Set<ARXNode> visible, final int[] optimum) {

        // Remove hidden from visible
        final Iterator<ARXNode> i = visible.iterator();
        while (i.hasNext()) {
            final ARXNode node = i.next();
            if (!isAllowed(lattice, node)) {
                i.remove();
            }
        }

        // Build sets
        @SuppressWarnings("unchecked")
		final Set<Integer>[] required = new HashSet[optimum.length];
        for (int j = 0; j < optimum.length; j++) {
            required[j] = new HashSet<Integer>();
        }
        for (final ARXNode node : visible) {
            for (int j = 0; j < optimum.length; j++) {
                required[j].add(node.getTransformation()[j]);
            }
        }

        // Clean the settings
        for (int j = 0; j < optimum.length; j++) {
            final Iterator<Integer> it = generalizations[j].iterator();
            while (it.hasNext()) {
                final int l = it.next();
                if (!required[j].contains(l)) {
                    it.remove();
                }
            }
        }
    }
    

    /**
     * Counts the number of visible nodes.
     *
     * @param lattice
     * @param visible
     * @param hidden
     * @return
     */
    private int count(final ARXLattice lattice, final Set<ARXNode> visible, final Set<ARXNode> hidden) {
        final Iterator<ARXNode> i = hidden.iterator();
        while (i.hasNext()) {
            final ARXNode node = i.next();
            if (isAllowed(lattice, node)) {
                i.remove();
                visible.add(node);
            }
        }
        return visible.size();
    }

    /**
     * 
     *
     * @param stream
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
        
        stream.defaultReadObject();
        
        if (this.minInformationLoss < 0d) {
            this.minInformationLoss = 0d;
        }
        if (this.minInformationLoss > 1d) {
            this.minInformationLoss = 1d;
        }
        if (this.maxInformationLoss < 0d) {
            this.maxInformationLoss = 0d;
        }
        if (this.maxInformationLoss > 1d) {
            this.maxInformationLoss = 1d;
        }
        
    }
}
