/*
 * 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.view.impl.wizard;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.deidentifier.arx.DataType;
import org.deidentifier.arx.DataType.DataTypeWithRatioScale;
import org.deidentifier.arx.aggregates.AggregateFunction;
import org.deidentifier.arx.aggregates.HierarchyBuilderGroupingBased.Group;
import org.deidentifier.arx.aggregates.HierarchyBuilderGroupingBased.Level;
import org.deidentifier.arx.aggregates.HierarchyBuilderIntervalBased;
import org.deidentifier.arx.aggregates.HierarchyBuilderIntervalBased.Interval;
import org.deidentifier.arx.aggregates.HierarchyBuilderOrderBased;
import org.deidentifier.arx.gui.resources.Resources;
import org.deidentifier.arx.gui.view.impl.wizard.HierarchyWizard.HierarchyWizardView;

/**
 * A base-class for grouping-based builders, i.e., order-based and interval-based builders
 * @author Fabian Prasser
 *
 * @param <T>
 */
public abstract class HierarchyWizardModelGrouping<T> extends HierarchyWizardModelAbstract<T>{
    
    /**
     * This class represents a group.
     *
     * @author Fabian Prasser
     * @param <U>
     */
    public static class HierarchyWizardGroupingGroup<U> {
        
        /** Var. */
        public int size;
        
        /** Var. */
        public AggregateFunction<U> function;
        
        /**
         * 
         *
         * @param group
         */
        public HierarchyWizardGroupingGroup(Group<U> group) {
            this.size = group.getSize();
            this.function = group.getFunction();
        }

        /**
         * 
         *
         * @param size
         * @param function
         */
        public HierarchyWizardGroupingGroup(int size, AggregateFunction<U> function) {
            this.size = size;
            this.function = function;
        }
    }

    /**
     * This class represents an interval.
     *
     * @author Fabian Prasser
     * @param <U>
     */
    public static class HierarchyWizardGroupingInterval<U> {

        /** Min */
        public U                    min;

        /** Max */
        public U                    max;

        /** Functio */
        public AggregateFunction<U> function;

        /**
         * 
         *
         * @param interval
         */
        public HierarchyWizardGroupingInterval(Interval<U> interval) {
            this.min = interval.getMin();
            this.max = interval.getMax();
            this.function = interval.getFunction();
        }

        /**
         * 
         *
         * @param min
         * @param max
         * @param function
         */
        public HierarchyWizardGroupingInterval(U min, U max, AggregateFunction<U> function) {
            this.min = min;
            this.max = max;
            this.function = function;
        }
    }
    
    /**
     * This class represents an adjustment.
     *
     * @author Fabian Prasser
     * @param <U>
     */
    public static class HierarchyWizardGroupingRange<U> {

        /** Snap */
        public U snapBound;

        /** Coding */
        public U bottomTopCodingBound;

        /** Extreme */
        public U minMaxBound;

        /**
         * 
         *
         * @param type
         * @param lower
         */
        @SuppressWarnings("unchecked")
        public HierarchyWizardGroupingRange(DataType<U> type, boolean lower){
            DataTypeWithRatioScale<U> dtype = (DataTypeWithRatioScale<U>)type;
            if (lower){
                this.snapBound = dtype.getMinimum();
                this.bottomTopCodingBound = dtype.getMinimum();
                this.minMaxBound = dtype.getMinimum();
            } else {
                this.snapBound = dtype.getMaximum();
                this.bottomTopCodingBound = dtype.getMaximum();
                this.minMaxBound = dtype.getMaximum();
            }
        }
    }

    /** Var. */
    private List<HierarchyWizardGroupingInterval<T>>    intervals     = new ArrayList<HierarchyWizardGroupingInterval<T>>();
    
    /** Var. */
    private List<List<HierarchyWizardGroupingGroup<T>>> groups        = new ArrayList<List<HierarchyWizardGroupingGroup<T>>>();
    
    /** Var. */
    private DataType<T>                                 type;
    
    /** Var. */
    private boolean                                     showIntervals = true;
    
    /** Var. */
    private AggregateFunction<T>                        function;
    
    /** Var. */
    private HierarchyWizardGroupingRange<T>             lower         = null;
    
    /** Var. */
    private HierarchyWizardGroupingRange<T>             upper         = null;
    
    /** Var. */
    private Object                                      selected      = null;
    
    /** Var. */
    private HierarchyWizardEditorRenderer<T>            renderer      = new HierarchyWizardEditorRenderer<T>(this);
    
    /** Var. */
    private List<HierarchyWizardView>                   components    = new ArrayList<HierarchyWizardView>();

    /**
     * Creates a new instance.
     *
     * @param data
     * @param type
     * @param intervals
     */
    @SuppressWarnings("unchecked")
    public HierarchyWizardModelGrouping(String[] data, DataType<T> type, boolean intervals){
        super(data);
        this.type = type;
        this.showIntervals = intervals;
        if (intervals) {
            
            // Check
            if (!(type instanceof DataTypeWithRatioScale)) {
                throw new IllegalArgumentException(Resources.getMessage("HierarchyWizardModelGrouping.0")); //$NON-NLS-1$
            }
            
            // Prepare
            DataTypeWithRatioScale<T> dtype = (DataTypeWithRatioScale<T>)type;
            this.lower = new HierarchyWizardGroupingRange<T>(type, true);
            this.upper = new HierarchyWizardGroupingRange<T>(type, false);
            this.function = AggregateFunction.forType(type).createIntervalFunction(true, false);
            
            // Initialize
            if (data != null){
                T min = null;
                T max = null;
                for (String date : data) {
                    T value = dtype.parse(date);
                    if (value != null) {
                        if (min==null || dtype.compare(value, min) < 0) min = value;
                        if (max==null || dtype.compare(value, max) > 0) max = value;
                    }
                }
                
                if (equals(type, DataType.INTEGER)) {
                    max = dtype.add(max, (T)Long.valueOf(1l)); // Add 1
                } else if (equals(type, DataType.DECIMAL)) {
                    max = dtype.add(max, (T)Double.valueOf(1d)); // Add 1
                } else if (equals(type, DataType.DATE)) {
                    max = dtype.add(max, (T)new Date(3600l * 1000l)); // Add 1 day
                }
                
                this.lower.minMaxBound = min;
                this.lower.snapBound = min;
                this.lower.bottomTopCodingBound = min;
                this.upper.minMaxBound = max;
                this.upper.snapBound = max;
                this.upper.bottomTopCodingBound = max;
                this.intervals.add(new HierarchyWizardGroupingInterval<T>(min, max, this.function));
            } else {
                this.intervals.add(new HierarchyWizardGroupingInterval<T>(dtype.getMinimum(), dtype.getMaximum(), this.function));
            }
        } else {
            this.function = AggregateFunction.forType(type).createSetFunction();
            this.groups.add(new ArrayList<HierarchyWizardGroupingGroup<T>>());
            this.groups.get(0).add(new HierarchyWizardGroupingGroup<T>(1, this.function));
        }
        this.update();
    }
    

    /**
     * Adds an element after the given one.
     *
     * @param selected
     */
    @SuppressWarnings("unchecked")
    public void addAfter(Object selected) {
        if (selected instanceof HierarchyWizardGroupingInterval){
            int index= intervals.indexOf(selected);
            if (index != -1){
                T bound = ((HierarchyWizardGroupingInterval<T>)selected).max;
                HierarchyWizardGroupingInterval<T> interval = new HierarchyWizardGroupingInterval<T>(bound, bound, this.function);
                intervals.add(index + 1, interval);
                this.setSelectedElement(interval);
                update();
                return;
            }
        } else if (selected instanceof HierarchyWizardGroupingGroup){
            for (List<HierarchyWizardGroupingGroup<T>> list : groups){
                int index= list.indexOf(selected);
                if (index != -1){
                    HierarchyWizardGroupingGroup<T> group = new HierarchyWizardGroupingGroup<T>(1, this.function);
                    list.add(index + 1, group);
                    this.setSelectedElement(group);
                    update();
                    return;
                }
            }
        }
        return;
    }

    /**
     * Adds an element before the given one.
     *
     * @param selected
     */
    @SuppressWarnings("unchecked")
    public void addBefore(Object selected) {
        if (selected instanceof HierarchyWizardGroupingInterval){
            int index= intervals.indexOf(selected);
            if (index != -1){
                T bound = ((HierarchyWizardGroupingInterval<T>)selected).min;
                HierarchyWizardGroupingInterval<T> interval = new HierarchyWizardGroupingInterval<T>(bound, bound, this.function);
                intervals.add(index, interval);
                this.setSelectedElement(interval);
                update();
                return;
            }
        } else if (selected instanceof HierarchyWizardGroupingGroup){
            for (List<HierarchyWizardGroupingGroup<T>> list : groups){
                int index= list.indexOf(selected);
                if (index != -1){
                    HierarchyWizardGroupingGroup<T> group = new HierarchyWizardGroupingGroup<T>(1, this.function);
                    list.add(index, group);
                    this.setSelectedElement(group);
                    update();
                    return;
                }
            }
        }
    }
 
    /**
     * Adds a column.
     *
     * @param selected
     */
    public void addRight(Object selected) {
        int index = 0;
        if (selected instanceof HierarchyWizardGroupingGroup){
            for (int i=0; i<groups.size(); i++){
                if (groups.get(i).indexOf(selected) != -1) {
                    index = i+1; 
                }
            }
        }
        
        List<HierarchyWizardGroupingGroup<T>> list = new ArrayList<HierarchyWizardGroupingGroup<T>>();
        groups.add(index, list);
        HierarchyWizardGroupingGroup<T> group = new HierarchyWizardGroupingGroup<T>(1, function);
        list.add(group);
        this.setSelectedElement(group);
        update();
    }

    /**
     * @return the type
     */
    public DataType<T> getDataType() {
        return type;
    }
    
    /**
     * Returns the default aggregate function.
     *
     * @return
     */
    public AggregateFunction<T> getDefaultFunction(){
        return this.function;
    }
    
    /**
     * @return the intervals
     */
    public List<HierarchyWizardGroupingInterval<T>> getIntervals() {
        return intervals;
    }

    /**
     * @return the lower
     */
    public HierarchyWizardGroupingRange<T> getLowerRange() {
        return lower;
    }

    /**
     * @return the groups
     */
    public List<List<HierarchyWizardGroupingGroup<T>>> getModelGroups() {
        return groups;
    }

    /**
     * @return the renderer
     */
    public HierarchyWizardEditorRenderer<T> getRenderer() {
        return renderer;
    }
    
    /**
     * @return the selected
     */
    public Object getSelectedElement() {
        return selected;
    }
    
    /**
     * @return the upper
     */
    public HierarchyWizardGroupingRange<T> getUpperRange() {
        return upper;
    }
    
    /**
     * Is this the first interval.
     *
     * @param interval
     * @return
     */
    public boolean isFirst(HierarchyWizardGroupingInterval<T> interval){
        return intervals.indexOf(interval) == 0;
    }

    /**
     * Is this the last interval.
     *
     * @param interval
     * @return
     */
    public boolean isLast(HierarchyWizardGroupingInterval<T> interval){
        return intervals.indexOf(interval) == intervals.size()-1;
    }

    /**
     * @return the showIntervals
     */
    public boolean isShowIntervals() {
        return showIntervals;
    }

    /**
     * Merges the interval down.
     *
     * @param selected
     */
    public void mergeDown(Object selected) {
        if (selected instanceof HierarchyWizardGroupingInterval){
            int index = intervals.indexOf(selected);
            if (index != -1) {
                AggregateFunction<T> function = intervals.get(index).function;
                T min = intervals.get(index - 1).min;
                T max = intervals.get(index).max;
                intervals.remove(index - 1);
                intervals.remove(index - 1);
                HierarchyWizardGroupingInterval<T> interval = new HierarchyWizardGroupingInterval<T>(min, max, function);
                intervals.add(index - 1, interval);
                this.setSelectedElement(interval);
                update();
            }
        }
    }

    /**
     * Merges the interval up.
     *
     * @param selected
     */
    public void mergeUp(Object selected) {
        if (selected instanceof HierarchyWizardGroupingInterval){
            int index = intervals.indexOf(selected);
            if (index != -1) {
                AggregateFunction<T> function = intervals.get(index).function;
                T min = intervals.get(index).min;
                T max = intervals.get(index+1).max;
                intervals.remove(index);
                intervals.remove(index);
                HierarchyWizardGroupingInterval<T> interval = new HierarchyWizardGroupingInterval<T>(min, max, function);
                intervals.add(index, interval);
                this.setSelectedElement(interval);
                update();
            }
        }
    }

    /**
     * Parses an interval-based spec.
     *
     * @param builder
     */
    public void parse(HierarchyBuilderIntervalBased<T> builder){
        this.type = builder.getDataType();
        this.showIntervals = true;
        this.lower.minMaxBound = builder.getLowerRange().getMinMaxValue();
        this.lower.snapBound = builder.getLowerRange().getSnapFrom();
        this.lower.bottomTopCodingBound = builder.getLowerRange().getBottomTopCodingFrom();
        this.upper.minMaxBound = builder.getUpperRange().getMinMaxValue();
        this.upper.snapBound = builder.getUpperRange().getSnapFrom();
        this.upper.bottomTopCodingBound = builder.getUpperRange().getBottomTopCodingFrom();
        this.function = builder.getDefaultFunction();
        this.intervals.clear();
        this.groups.clear();
        for (Interval<T> interval : builder.getIntervals()) {
            this.intervals.add(new HierarchyWizardGroupingInterval<T>(interval));
        }
        for (Level<T> level : builder.getLevels()){
            List<HierarchyWizardGroupingGroup<T>> list = new ArrayList<HierarchyWizardGroupingGroup<T>>();
            this.groups.add(list);
            for (Group<T> group : level.getGroups()) {
                list.add(new HierarchyWizardGroupingGroup<T>(group));
            }
        }
        update();
    }

    /**
     * Parses an order-based spec.
     *
     * @param builder
     * @throws IllegalArgumentException
     */
    public void parse(HierarchyBuilderOrderBased<T> builder) throws IllegalArgumentException{
        
        if (builder.getComparator() != null) {
            try {
                Arrays.sort(this.data, builder.getComparator());
            } catch (Exception e){
                throw new IllegalArgumentException(Resources.getMessage("HierarchyWizardModelGrouping.1")); //$NON-NLS-1$
            }
        }
        this.showIntervals = false;
        this.function = builder.getDefaultFunction();
        this.intervals.clear();
        this.groups.clear();
        this.lower = null;
        this.upper = null;
        for (Level<T> level : builder.getLevels()){
            List<HierarchyWizardGroupingGroup<T>> list = new ArrayList<HierarchyWizardGroupingGroup<T>>();
            this.groups.add(list);
            for (Group<T> group : level.getGroups()) {
                list.add(new HierarchyWizardGroupingGroup<T>(group));
            }
        }
        update();
    }

    /**
     * Registers a part of the UI.
     *
     * @param component
     */
    public void register(HierarchyWizardView component){
        this.components.add(component);
    }

    /**
     * Removes the given object.
     *
     * @param selected
     */
    public void remove(Object selected) {
        if (selected instanceof HierarchyWizardGroupingInterval){
            Iterator<HierarchyWizardGroupingInterval<T>> iter = intervals.iterator();
            while (iter.hasNext()){
                if (iter.next().equals(selected)) {
                    iter.remove();
                    selected = null;
                    update();
                    return;
                }
            }
        } else if (selected instanceof HierarchyWizardGroupingGroup){
            for (List<HierarchyWizardGroupingGroup<T>> list : groups){
                Iterator<HierarchyWizardGroupingGroup<T>> iter = list.iterator();
                while (iter.hasNext()){
                    if (iter.next().equals(selected)) {
                        iter.remove();
                        if (list.isEmpty()) {
                            groups.remove(list);
                        }
                        selected = null;
                        update();
                        return;
                    }
                }
            }
        }
    }

    /**
     * Sets the default aggregate function.
     *
     * @param function
     */
    public void setDefaultFunction(AggregateFunction<T> function){
        AggregateFunction<T> old = this.function;
        this.function = function;
        // Update
        for (HierarchyWizardGroupingInterval<T> interval : intervals) {
            if (interval.function == old){
                interval.function = function;
            }
        }
        for (List<HierarchyWizardGroupingGroup<T>> list : groups) {
            for (HierarchyWizardGroupingGroup<T> group : list) {
                if (group.function == old){
                    group.function = function;
                }
            }
        }
    }

    /**
     * Updates the selected element.
     *
     * @param selected
     */
    public void setSelectedElement(Object selected) {
        this.selected = selected;
    }

    /**
     * Update the model and all UI components.
     */
    @Override
    public void update(){
        super.update();
        renderer.update();
        updateUI(null);
    }

    /**
     * Update the model and all UI components, apart from the sender.
     *
     * @param sender
     */
    public void update(HierarchyWizardView sender){
        super.update();
        renderer.update();
        updateUI(sender);
    }
    
    /**
     * Update the UI components.
     */
    @Override
    public void updateUI(HierarchyWizardView sender){
        for (HierarchyWizardView c : components){
            if (c != sender) {
                c.update();
            }
        }
    }

    /**
     * Simple comparison of data types.
     *
     * @param type
     * @param other
     * @return
     */
    private boolean equals(DataType<?> type, DataType<?> other){
        return type.getDescription().getLabel().equals(other.getDescription().getLabel());
    }
}
