/*
 * ARX: Powerful Data Anonymization
 * Copyright 2012 - 2017 Fabian Prasser, Florian Kohlmayer 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;

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

import org.deidentifier.arx.DataHandleInternal.InterruptHandler;
import org.deidentifier.arx.aggregates.StatisticsBuilder;
import org.deidentifier.arx.framework.data.Dictionary;

/**
 * An implementation of the DataHandle interface for input data.
 *
 * @author Fabian Prasser
 * @author Florian Kohlmayer
 */
public class DataHandleInput extends DataHandle {

    /** The data. */
    protected int[][]    data       = null;

    /** The dictionary. */
    protected Dictionary dictionary = null;

    /** The data. */
    private int[][]      dataGH     = null;

    /** The data. */
    private int[][]      dataDI     = null;

    /** The data. */
    private int[][]      dataIS     = null;
    
    /** Is this handle locked?. */
    private boolean      locked     = false;

    /**
     * Creates a new data handle.
     *
     * @param data
     */
    protected DataHandleInput(final Data data) {
        
        // Obtain and check iterator
        final Iterator<String[]> iterator = data.iterator();
        if (!iterator.hasNext()) { 
            throw new IllegalArgumentException("Data object is empty!"); 
        }

        // Register
        this.setRegistry(new DataRegistry());
        this.getRegistry().updateInput(this);
        this.definition = data.getDefinition().clone();

        // Obtain header
        final String[] columns = iterator.next();
        super.header = Arrays.copyOf(columns, columns.length);

        // Init dictionary
        this.dictionary = new Dictionary(header.length);

        // Encode data
        List<int[]> vals = new ArrayList<int[]>();
        while (iterator.hasNext()) {

            // Process a tuple
            final String[] strings = iterator.next();
            final int[] tuple = new int[header.length];
            for (int i = 0; i < strings.length; i++) {
                tuple[i] = dictionary.register(i, strings[i]);
            }
            vals.add(tuple);
        }

        // Build array
        this.data = vals.toArray(new int[vals.size()][]);

        // finalize dictionary
        this.dictionary.finalizeAll();

        // Create datatype array
        this.dataTypes = getDataTypeArray();
    }

    @Override
    public String getAttributeName(final int column) {
        checkRegistry();
        checkColumn(column);
        return header[column];
    }

    @Override
    public int getGeneralization(final String attribute) {
        checkRegistry();
        return 0;
    }

    @Override
    public int getNumColumns() {
        checkRegistry();
        return header.length;
    }

    @Override
    public int getNumRows() {
        checkRegistry();
        return data.length;
    }

    @Override
    public StatisticsBuilder getStatistics() {
        return new StatisticsBuilder(new DataHandleInternal(this));
    }

    @Override
    public String getValue(final int row, final int column) {
        checkRegistry();
        checkColumn(column);
        checkRow(row, data.length);
        return internalGetValue(row, column, false);
    }

    @Override
    public boolean isOutlier(int row){
        return false;
    }

    @Override
    public Iterator<String[]> iterator() {
        checkRegistry();
        return new Iterator<String[]>() {

            int index = -1;

            @Override
            public boolean hasNext() {
                return (index < data.length);
            }

            @Override
            public String[] next() {
                if (index == -1) {
                    index++;
                    return header;
                } else {
                    final String[] result = new String[header.length];
                    for (int i = 0; i < result.length; i++) {
                        result[i] = getValue(index, i);
                    }
                    index++;
                    return result;
                }
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException("Remove is not supported by this iterator");
            }
        };
    }
    
    /**
     * Swaps two rows.
     *
     * @param row1
     * @param row2
     * @param data
     */
    private void swap(int row1, int row2, int[][] data){
        final int[] temp = data[row1];
        data[row1] = data[row2];
        data[row2] = temp;
    }

    /**
     * Releases all resources.
     */
    protected void doRelease() {
        this.setLocked(false);
        dataGH = null;
        dataDI = null;
        dataIS = null;
    }
    
    @Override
    protected DataType<?> getBaseDataType(final String attribute) {
        return this.getDataType(attribute);
    }

    @Override
    protected ARXConfiguration getConfiguration() {
        return null;
    }

    @Override
    protected DataType<?>[][] getDataTypeArray() {
        checkRegistry();
        DataType<?>[][] dataTypes = new DataType[1][header.length];
        for (int i = 0; i < header.length; i++) {
            final DataType<?> type = definition.getDataType(header[i]);
            if (type != null) {
                dataTypes[0][i] = type;
            } else {
                dataTypes[0][i] = DataType.STRING;
            }
        }
        return dataTypes;
    }
    
    @Override
    protected String[] getDistinctValues(final int column, final boolean ignoreSuppression, InterruptHandler handler) {
        checkRegistry();
        handler.checkInterrupt();
        checkColumn(column);
        handler.checkInterrupt();
        final String[] dict = dictionary.getMapping()[column];
        handler.checkInterrupt();
        final String[] vals = new String[dict.length];
        handler.checkInterrupt();
        System.arraycopy(dict, 0, vals, 0, vals.length);
        return vals;
    }

    /**
     * Returns the input buffer
     * @return
     */
    protected int[][] getInputBuffer() {
        checkRegistry();
        return this.dataGH;
    }
    
    @Override
    protected String internalGetValue(final int row, final int column, final boolean ignoreSuppression) {
        return dictionary.getMapping()[column][data[row][column]];
    }
    
    @Override
    protected boolean internalReplace(int column,
                                      String original,
                                      String replacement) {

        String[] values = dictionary.getMapping()[column];
        boolean found = false;
        for (int i = 0; i < values.length; i++) {
            if (values[i].equals(original)) {
                values[i] = replacement;
                found = true;
            }
        }
        return found;
    }

    /**
     * Swaps the rows.
     *
     * @param row1
     * @param row2
     */
    protected void internalSwap(final int row1, final int row2) {

        // Check
        checkRow(row1, data.length);
        checkRow(row2, data.length);

        // Swap
        swap(row1, row2, data);
        if (dataGH != null) swap(row1, row2, dataGH);
        if (dataDI != null) swap(row1, row2, dataDI);
        if (dataIS != null) swap(row1, row2, dataIS);
    }

    /**
     * Is this handle locked?.
     *
     * @return
     */
    protected boolean isLocked(){
        return this.locked;
    }

    /**
     * Overrides the handles data definition.
     *
     * @param definition
     */
    protected void setDefinition(DataDefinition definition) {
        this.definition = definition;
    }

    /**
     * Lock/unlock this handle.
     *
     * @param locked
     */
    protected void setLocked(boolean locked){
        this.locked = locked;
    }

    /**
     * Update the definition.
     *
     * @param data
     */
    protected void update(Data data){

        if (!this.isLocked()) {
            this.definition = data.getDefinition().clone();
            this.dataTypes = getDataTypeArray();
            this.definition.setLocked(true);
        }
    }

    /**
     * Updates the definition with further data to swap.
     *
     * @param dataGH
     * @param dataDI
     * @param dataIS
     */
    protected void update(int[][] dataGH, int[][] dataDI, int[][] dataIS) {
        this.dataGH = dataGH;
        this.dataDI = dataDI;
        this.dataIS = dataIS;
    }
}
