/*
 * #%L
 * gitools-core
 * %%
 * Copyright (C) 2013 Universitat Pompeu Fabra - Biomedical Genomics group
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */
package org.gitools.matrix.model.hashmatrix;

import org.gitools.api.matrix.IMatrix;
import org.gitools.api.matrix.IMatrixDimension;
import org.gitools.api.matrix.IMatrixLayer;
import org.gitools.api.matrix.MatrixDimensionKey;
import org.gitools.matrix.model.AbstractMatrix;
import org.gitools.matrix.model.MatrixLayers;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class HashMatrix extends AbstractMatrix<MatrixLayers<IMatrixLayer>, HashMatrixDimension> {

    private Map<String, Map> values;

    public HashMatrix(MatrixLayers layers, MatrixDimensionKey... dimensions) {
        this(layers, createHashMatrixDimensions(dimensions));
    }

    public HashMatrix(MatrixLayers<? extends IMatrixLayer> layers, IMatrixDimension... dimensions) {
        this(layers, createHashMatrixDimensions(dimensions));
    }

    public HashMatrix(MatrixLayers<? extends IMatrixLayer> layers, HashMatrixDimension... dimensions) {
        super((MatrixLayers<IMatrixLayer>) layers, dimensions);

        this.values = new ConcurrentHashMap<>();
        for (IMatrixLayer layer : layers) {
            this.values.put(layer.getId(), new ConcurrentHashMap());
        }
    }

    @Override
    public <T> T get(IMatrixLayer<T> layer, String... identifiers) {

        Map result = values.get(layer.getId());
        if (result == null) {
            return null;
        }

        String identifier = identifiers[0];
        for (int i = 1; i < identifiers.length; i++) {

            if (identifier == null || !result.containsKey(identifier)) {
                return null;
            }

            result = (Map) result.get(identifier);
            identifier = identifiers[i];
        }

        if (identifier == null || !result.containsKey(identifier)) {
            return null;
        }

        return (T) result.get(identifier);
    }

    public <T> void set(IMatrixLayer<T> layer, T value, String... identifiers) {
        assert identifiers.length == getDimensionKeys().length : "Matrix of dimension " + getDimensionKeys().length + " and position of " + identifiers.length + " elements";

        if (value == null) {
            return;
        }

        Map result = values.get(layer.getId());
        if (result == null) {
            throw new UnsupportedOperationException("Layer '" + layer.getId() + "' not found");
        }

        String identifier = identifiers[0];
        HashMatrixDimension dimension = getDimension(getDimensionKeys()[0]);

        for (int i = 1; i < identifiers.length; i++) {

            if (!result.containsKey(identifier)) {
                result.put(identifier, new ConcurrentHashMap());
                dimension.add(identifier);
            }

            result = (Map) result.get(identifier);

            identifier = identifiers[i];
            dimension = getDimension(getDimensionKeys()[i]);
        }

        if (!result.containsKey(identifier)) {
            dimension.add(identifier);
        }

        result.put(identifier, value);
        hasChanged = true;

    }

    /**
     *  You must call this method after load all values into the matrix.
     *  Some implementations must prepare the matrix to be read and improve
     *  the read performance. Also the @see isChanged() will be true only
     *  if there is any change after this method has been call.
     */
    public void init() {
        this.hasChanged = false;
    }

    public void addLayer(IMatrixLayer layer) {
        getLayers().add(layer);
        values.put(layer.getId(), new ConcurrentHashMap());
    }

    public void removeLayer(IMatrixLayer layer) {
        getLayers().remove(layer);
        values.remove(layer.getId());
    }

    public void copyLayerValues(String layerId, HashMatrix fromMatrix) {

        if (getLayers().get(layerId) == null) {
            throw new UnsupportedOperationException("Unknown '" + layerId + "' layer in this matrix");
        }

        // Copy values map
        Map values = fromMatrix.values.get(layerId);
        this.values.put(layerId, values);

        // Update dimension identifiers
        for (MatrixDimensionKey dimensionKey : getDimensionKeys()) {
            HashMatrixDimension thisDimension = getDimension(dimensionKey);
            HashMatrixDimension fromDimension = fromMatrix.getDimension(dimensionKey);

            if (fromDimension == null) {
                throw new UnsupportedOperationException("Impossible to copy a matrix with different dimensions.");
            }

            for (String identifier : fromDimension) {
                thisDimension.add(identifier);
            }
        }

        this.hasChanged = true;
    }

    private static HashMatrixDimension[] createHashMatrixDimensions(MatrixDimensionKey[] identifiers) {
        HashMatrixDimension[] dimensions = new HashMatrixDimension[identifiers.length];

        for (int i = 0; i < identifiers.length; i++) {
            dimensions[i] = new HashMatrixDimension(identifiers[i]);
        }

        return dimensions;
    }

    private static HashMatrixDimension[] createHashMatrixDimensions(IMatrixDimension[] dimensions) {
        HashMatrixDimension[] hashDimensions = new HashMatrixDimension[dimensions.length];

        for (int i = 0; i < dimensions.length; i++) {
            IMatrixDimension dimension = dimensions[i];

            if (dimension instanceof HashMatrixDimension) {
                hashDimensions[i] = (HashMatrixDimension) dimension;
            } else {
                hashDimensions[i] = new HashMatrixDimension(dimensions[i].getId(), dimensions[i]);
            }
        }

        return hashDimensions;
    }

    @Override
    public IMatrix subset(IMatrixDimension... dimensionSubsets) {

        Map<MatrixDimensionKey, HashMatrixDimension> allDimensions = new HashMap<>(getDimensionKeys().length);

        // Load all dimensions
        for (MatrixDimensionKey key : getDimensionKeys()) {
            allDimensions.put(key, getDimension(key));
        }

        // Override subset dimensions
        for (IMatrixDimension dimension : dimensionSubsets) {

            if (dimension instanceof HashMatrixDimension) {
                allDimensions.put(dimension.getId(), (HashMatrixDimension) dimension);
            } else {
                allDimensions.put(dimension.getId(), new HashMatrixDimension(dimension.getId(), dimension));
            }
        }

        return new SubMatrix(getLayers(), allDimensions.values());
    }

    private class SubMatrix extends AbstractMatrix<MatrixLayers<IMatrixLayer>, HashMatrixDimension> {

        public SubMatrix(MatrixLayers layers, Collection<HashMatrixDimension> identifiers) {
            super(layers, (HashMatrixDimension[]) identifiers.toArray());
        }

        @Override
        public <T> T get(IMatrixLayer<T> layer, String... identifiers) {
            return HashMatrix.this.get(layer, identifiers);
        }

        @Override
        public <T> void set(IMatrixLayer<T> layer, T value, String... identifiers) {
            throw new UnsupportedOperationException("The subset matrix are read only matrix");
        }

        @Override
        public boolean isChanged() {
            return false;
        }
    }
}
