/*
 * #%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.heatmap.header;

import com.google.common.base.Function;
import com.google.common.collect.Lists;
import org.apache.commons.math3.util.FastMath;
import org.gitools.heatmap.HeatmapDimension;
import org.gitools.heatmap.decorator.DetailsDecoration;
import org.gitools.matrix.model.matrix.AnnotationMatrix;
import org.gitools.utils.textpattern.TextPattern;

import javax.xml.bind.annotation.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement
public class HierarchicalClusterHeatmapHeader extends HeatmapHeader {

    public static final String PROPERTY_INTERACTION_LEVEL = "PROPERTY_INTERACTION_LEVEL";
    @XmlElementWrapper(name="clusterLevels")
    @XmlElement(name="levels")
    private List<HeatmapColoredLabelsHeader> clusterLevels;

    @XmlTransient
    private int interactionLevel = -1;

    @XmlTransient
    private boolean reportLastInteraction;

    public static final String PROPERTY_VISIBLE_LEVELS = "visibleLevels";
    @XmlTransient
    private String visibleLevels;

    @XmlTransient
    private String visibleLevelsFormat = "(\\d+(\\-\\d+)?(,(?!$))?)+";

    public static final String PROPERTY_MAX_LEVELS = "maxLevels";
    @XmlTransient
    private int maxLevels = -1;

    public static final String PROPERTY_COLOR_PALETTE = "colorPalette";
    @XmlElement
    private String colorPalette = "Default";

    @XmlElement
    private HierarchicalCluster hierarchicalCluster;

    public HierarchicalClusterHeatmapHeader() {
        super();
        reportLastInteraction = false;
    }

    public HierarchicalClusterHeatmapHeader(HeatmapDimension hdim) {
        super(hdim);
        clusterLevels = new ArrayList<>();
        reportLastInteraction = false;
        setMargin(1);
    }

    public static void createHierarchicalLevelsHeaders(HierarchicalClusterHeatmapHeader hierarchicalHeader,
                                                       int maxLevels,
                                                       String annotationPrefix) {
        int currentLevel = 0;
        HeatmapDimension clusteringDimension = hierarchicalHeader.getHeatmapDimension();
        AnnotationMatrix annotationMatrix = clusteringDimension.getAnnotations();
        Map<Integer, List<HierarchicalCluster>> clustersMapPerLevel = new HashMap<>();
        List<HierarchicalCluster> children = hierarchicalHeader.getHierarchicalCluster().getChildren();
        while (currentLevel < maxLevels) {
            currentLevel++;

            List<HierarchicalCluster> nextLevel = new ArrayList<>();
            for (HierarchicalCluster cluster : children) {
                if (!cluster.getChildren().isEmpty()) {
                    for (String identifier : cluster.getIdentifiers()) {
                        annotationMatrix.setAnnotation(identifier, annotationPrefix + currentLevel, cluster.getName());
                    }
                }
                nextLevel.addAll(cluster.getChildren());
            }

            clustersMapPerLevel.put(currentLevel, children);
            children = nextLevel;

            if (children.isEmpty()) {
                currentLevel--;
                break;
            }
        }

        // Hierarchical clustering headers
        int depth = FastMath.min(maxLevels, currentLevel);
        for (int l = depth; l >= 1; l--) {
            HeatmapColoredLabelsHeader levelHeader = new HeatmapColoredLabelsHeader(clusteringDimension);

            List<HierarchicalCluster> clusters = clustersMapPerLevel.get(l);
            List<ColoredLabel> coloredLabels = new ArrayList<>(clusters.size());
            for (HierarchicalCluster cluster : clusters) {
                coloredLabels.add(new ColoredLabel(cluster.getName(), new Color(cluster.getColor())));
            }

            levelHeader.setClusters(coloredLabels);


            levelHeader.setTitle(annotationPrefix + l);
            levelHeader.setSize(7);
            levelHeader.setAnnotationPattern("${" + annotationPrefix + l + "}");
            hierarchicalHeader.addLevel(levelHeader);
        }
    }

    public void addLevel(HeatmapColoredLabelsHeader level) {
        level.setMargin(1);
        clusterLevels.add(level);
    }

    @Override
    public void init(HeatmapDimension heatmapDimension) {
        super.init(heatmapDimension);
        for (HeatmapColoredLabelsHeader levels : clusterLevels) {
            levels.init(heatmapDimension);
        }
    }

    @Override
    public void setMargin(int margin) {
        for (HeatmapColoredLabelsHeader levelHeader : getClusterLevels()) {
            levelHeader.setMargin(margin);
        }
        super.setMargin(margin);
    }

    public List<HeatmapColoredLabelsHeader> getClusterLevels() {
        return clusterLevels;
    }

    /* The thickness of the color band */
    public int getThickness() {
        return clusterLevels.size() > 0 ? clusterLevels.get(0).getThickness() : 1;
    }

    /* The thickness of the color band */
    public void setThickness(int thickness) {
        for (HeatmapColoredLabelsHeader level : clusterLevels) {
            level.setThickness(thickness);
        }
    }

    /* Separate different clusters with a grid */
    public boolean isSeparationGrid() {
        return clusterLevels.size() > 0 ? clusterLevels.get(0).isSeparationGrid() : true;
    }

    /* Separate different clusters with a grid */
    public void setSeparationGrid(boolean separationGrid) {
        for (HeatmapColoredLabelsHeader level : clusterLevels) {
            level.setSeparationGrid(separationGrid);
        }
    }

    public boolean isForceLabelColor() {
        return clusterLevels.size() > 0 ? clusterLevels.get(0).isForceLabelColor() : true;
    }


    public void setForceLabelColor(boolean forceLabelColor) {
        for (HeatmapColoredLabelsHeader level : clusterLevels) {
            level.setSeparationGrid(forceLabelColor);
        }
    }

    @Override
    public Font getFont() {
        return clusterLevels.size() > 0 ? clusterLevels.get(0).getFont() : null;

    }

    @Override
    public void setFont(Font font) {
        for (HeatmapColoredLabelsHeader level : clusterLevels) {
            level.setFont(font);
        }
    }

    @Override
    public int getSize() {
        size = 0;
        for (HeatmapColoredLabelsHeader level : clusterLevels) {
            if (level.isVisible()) {
                size += level.getSize();
            }
        }
        return size;
    }

    @Override
    public void populateDetails(List<DetailsDecoration> details, String identifier, boolean selected) {

        DetailsDecoration desiredDecoration = null;

        for (HeatmapColoredLabelsHeader level : Lists.reverse(clusterLevels)) {

            DetailsDecoration decoration = new DetailsDecoration(this.getTitle(),
                    getDescription(),
                    null, null, null);
            decoration.setReference(this);

            if (identifier != null) {
                level.reset();
                ColoredLabel cluster = level.getColoredLabel(identifier);

                Color clusterColor = cluster != null ? cluster.getColor() : getBackgroundColor();
                decoration.setBgColor(clusterColor);
                if (!cluster.getDisplayedLabel().equals("")) {
                    int levelIndex = clusterLevels.size() - clusterLevels.indexOf(level);
                    decoration.setValue("L" + levelIndex + ": " + cluster.getDisplayedLabel());
                    desiredDecoration = decoration;
                }
            }

            if (isReportLastInteraction() && clusterLevels.indexOf(level) == interactionLevel) {
                break;
            }

        }

        if (desiredDecoration != null) {
            desiredDecoration.setSelected(selected);
            desiredDecoration.setVisible(isVisible());
            details.add(desiredDecoration);
        }
    }

    @Override
    public Function<String, String> getIdentifierTransform() {
        return null;
    }

    public void setInteractionLevel(int interactionLevel) {
        int old = this.interactionLevel;
        this.interactionLevel = interactionLevel;
        firePropertyChange(PROPERTY_INTERACTION_LEVEL, old, interactionLevel);
    }

    public HeatmapColoredLabelsHeader getInteractionLevelHeader() {
        if (interactionLevel < 0) {
            return null;
        }
        return clusterLevels.get(interactionLevel);
    }

    public boolean isReportLastInteraction() {
        return reportLastInteraction;
    }

    public void setReportLastInteraction(boolean reportLastInteraction) {
        this.reportLastInteraction = reportLastInteraction;
        //firePropertyChange();
    }


    public void setHierarchicalCluster(HierarchicalCluster hierarchicalCluster) {
        this.hierarchicalCluster = hierarchicalCluster;
    }

    public HierarchicalCluster getHierarchicalCluster() {
        return hierarchicalCluster;
    }

    public HierarchicalCluster getHierarchicalCluster(String clusterName) {
        return hierarchicalCluster.getHierarchicalSubCluster(clusterName);
    }

    @Override
    public int getZoomStepSize() {
        return clusterLevels.size();
    }

    /**
     * The height/width
     */
    public void setSize(int size) {

        int change = 0;
        if (this.size < size) {
            change = 1;
        } else if (this.size > size) {
            change = -1;
        }

        if (change != 0) {
            for (HeatmapColoredLabelsHeader level: clusterLevels) {
                if (level.getSize() + change < 3) {
                    continue;
                }
                level.setSize(level.getSize() + change);
            }
        }

        int old = this.size;
        this.size = getSize();
        firePropertyChange(PROPERTY_SIZE, old, this.size);


    }

    public String getVisibleLevels() {

        if (visibleLevels == null) {
            visibleLevels = generateVisibleLevelsString();
        }
        return visibleLevels;
    }

    public void setVisibleLevels(String visibleLevels) {

        Pattern p = Pattern.compile(visibleLevelsFormat);
        Matcher m = p.matcher(visibleLevels);
        if (m.matches()) {
            //System.out.println("Valid pattern: " + visibleLevels);

            String old = this.visibleLevels;
            this.visibleLevels = visibleLevels;

            setLevelHeadersVisibility(visibleLevels);
            firePropertyChange(PROPERTY_VISIBLE_LEVELS, old, visibleLevels);
        }


    }

    public int getMaxLevels() {
        if (maxLevels == -1) {
            maxLevels = getClusterLevels().size();
        }
        return maxLevels;
    }

    public void setMaxLevels(int maxLevels) {
        int old = this.maxLevels;
        if (maxLevels < 10) {
            maxLevels = 10;
        }
        if (maxLevels != this.maxLevels) {
            String annoPrefix = getClusterLevels().get(1).getTitle().split(" L")[0];
            removeMetadata();
            HierarchicalClusterHeatmapHeader.createHierarchicalLevelsHeaders(this, maxLevels, annoPrefix);
            this.maxLevels = maxLevels;
            firePropertyChange(PROPERTY_MAX_LEVELS, old, maxLevels);

        }
    }

    public String getColorPalette() {
        if (colorPalette == null) {
            colorPalette = "Default";
        }
        return colorPalette;
    }

    public void setColorPalette(String colorPalette) {
        String old = this.colorPalette;
        this.colorPalette = colorPalette;
        if (!old.equals(colorPalette)) {
            // repaint
            String annoPrefix = getClusterLevels().get(1).getTitle().split("1")[0];
            removeMetadata();
            String oldname = this.hierarchicalCluster.getName();
            hierarchicalCluster.setName("");
            HierarchicalClusterNamer.nameClusters(this.hierarchicalCluster, colorPalette);
            hierarchicalCluster.setName(oldname);
            HierarchicalClusterHeatmapHeader.createHierarchicalLevelsHeaders(this, maxLevels, annoPrefix);
            firePropertyChange(PROPERTY_COLOR_PALETTE, old, colorPalette);
        }
    }


    private void setLevelHeadersVisibility(String visibleLevels) {

        List<Integer> visibleList = new ArrayList<>();
        for (String section : visibleLevels.split(",")) {
            if (!section.contains("-")) {
                visibleList.add(Integer.parseInt(section));
            } else {
                int start = Integer.parseInt(section.split("-")[0]);
                int end = Integer.parseInt(section.split("-")[1]);
                for (int i = start; i <= end; i++) {
                    visibleList.add(i);
                }
            }
        }

        int level = 0;
        for (HeatmapColoredLabelsHeader levelHeader : Lists.reverse(getClusterLevels())) {
            level++;
            boolean isVisible = visibleList.contains(level);
            levelHeader.setVisible(isVisible);
        }
    }

    private String generateVisibleLevelsString() {
        int level = 1;
        String visibleLevels = "";
        int lastReportedLevel = -1;
        int lastVisible = -1;
        for (HeatmapColoredLabelsHeader labelsHeader : getClusterLevels()) {

            if (labelsHeader.isVisible() && lastReportedLevel == -1) {
                visibleLevels = Integer.toString(level);
                lastReportedLevel = level;
                lastVisible = level;
            } else if (labelsHeader.isVisible()) {
                lastVisible = level;
            } else {
                if (lastVisible == lastReportedLevel) {
                    visibleLevels += ",";
                } else {
                    visibleLevels += "-" + Integer.toString(lastVisible) + ",";
                }
            }
            level++;
        }

        // after last loop
        if (lastVisible != lastReportedLevel) {
            visibleLevels += "-" + Integer.toString(lastVisible);
        }
        return visibleLevels;
    }

    public void removeMetadata() {
        AnnotationMatrix annotation = getHeatmapDimension().getAnnotations();
        for (HeatmapColoredLabelsHeader level : getClusterLevels()) {
            //annotation.removeAnnotations(level.getAnnotationPattern());

            for (TextPattern.Token token : new TextPattern(level.getAnnotationPattern()).getTokens()) {
                if (token instanceof TextPattern.VariableToken) {
                    annotation.removeAnnotations(((TextPattern.VariableToken) token).getVariableName());
                }
            }
        }
        clusterLevels.clear();
        annotation.getLabels();
    }

}
