/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2015-2020 Andres Almiray
 *
 * 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.kordamp.ikonli.javafx;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableIntegerProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.PaintConverter;
import javafx.css.converter.SizeConverter;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import org.kordamp.ikonli.Ikon;

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

import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;

/**
 * @author Andres Almiray
 */
public class StackedFontIcon extends StackPane implements Icon {
    private static final String KEY_STACKED_FONT_ICON_SIZE = StackedFontIcon.class.getName() + ".iconSize";

    private StyleableIntegerProperty iconSize;
    private StyleableObjectProperty<Paint> iconColor;
    private double[] iconSizes = new double[0];

    private ChangeListener<Number> iconSizeChangeListener = (v, o, n) -> setIconSizeOnChildren(n.intValue());
    private ChangeListener<Paint> iconColorChangeListener = (v, o, n) -> setIconColorOnChildren(n);

    public static void setIconSize(Node icon, double size) {
        if (icon != null && size >= 0d && size <= 1.0d) {
            icon.getProperties().put(KEY_STACKED_FONT_ICON_SIZE, size);
        }
    }

    public static double getIconSize(Node icon) {
        if (icon != null) {
            Object value = icon.getProperties().get(KEY_STACKED_FONT_ICON_SIZE);
            if (value instanceof Number) {
                return ((Number) value).doubleValue();
            }
        }
        return 1.0d;
    }

    private class NodeSizeListener implements MapChangeListener<Object, Object> {
        private Node node;

        private NodeSizeListener(Node node) {
            this.node = node;
        }

        @Override
        public void onChanged(Change<?, ?> change) {
            if (KEY_STACKED_FONT_ICON_SIZE.equals(String.valueOf(change.getKey()))) {
                int size = getChildren().size();
                for (int i = 0; i < size; i++) {
                    if (node == getChildren().get(i)) {
                        double value = 0;
                        Object valueAdded = change.getValueAdded();
                        if (valueAdded instanceof Number) {
                            value = ((Number) valueAdded).doubleValue();
                        } else {
                            value = Double.parseDouble(String.valueOf(valueAdded));
                        }
                        iconSizes[i] = value;
                        return;
                    }
                }
            }
        }
    }

    public StackedFontIcon() {
        getStyleClass().setAll("stacked-ikonli-font-icon");

        final String propertiesListenerKey = StackedFontIcon.class.getName() + "-" + System.identityHashCode(this);

        getChildren().addListener(new ListChangeListener<Node>() {
            @Override
            public void onChanged(Change<? extends Node> c) {
                while (c.next()) {
                    if (c.wasAdded()) {
                        int size = c.getTo() - c.getFrom();
                        // grow iconSizes by size
                        iconSizes = Arrays.copyOf(iconSizes, iconSizes.length + size);
                        // apply 1.0 [from..to]
                        for (int i = c.getFrom(); i < c.getTo(); i++) {
                            iconSizes[i] = getIconSize(c.getList().get(i));
                        }
                        for (Node node : c.getAddedSubList()) {
                            node.getProperties().put(propertiesListenerKey, new NodeSizeListener(node));
                        }
                    } else if (c.wasRemoved()) {
                        int size = c.getTo() - c.getFrom();
                        // shrink iconSizes by size
                        double[] newIconSizes = new double[iconSizes.length - size];
                        // copy [0..from]
                        int index = 0;
                        for (int i = 0; i < c.getFrom(); i++) {
                            newIconSizes[index++] = iconSizes[i];
                        }
                        // copy [to..-1]
                        for (int i = c.getTo(); i < iconSizes.length; i++) {
                            newIconSizes[index++] = iconSizes[i];
                        }
                        iconSizes = newIconSizes;
                        for (Node node : c.getRemoved()) {
                            node.getProperties().remove(propertiesListenerKey);
                        }
                    } else if (c.wasPermutated()) {
                        double[] newIconSizes = Arrays.copyOf(iconSizes, iconSizes.length);
                        for (int i = c.getFrom(); i <= c.getTo(); i++) {
                            newIconSizes[i] = c.getPermutation(i);
                        }
                        iconSizes = newIconSizes;
                    }
                }
            }
        });
    }

    public IntegerProperty iconSizeProperty() {
        if (iconSize == null) {
            iconSize = new StyleableIntegerProperty(16) {
                @Override
                public CssMetaData getCssMetaData() {
                    return StyleableProperties.ICON_SIZE;
                }

                @Override
                public Object getBean() {
                    return StackedFontIcon.this;
                }

                @Override
                public String getName() {
                    return "iconSize";
                }
            };
            iconSize.addListener(iconSizeChangeListener);
        }
        return iconSize;
    }

    public ObjectProperty<Paint> iconColorProperty() {
        if (iconColor == null) {
            iconColor = new StyleableObjectProperty<Paint>(Color.BLACK) {
                @Override
                public CssMetaData getCssMetaData() {
                    return StyleableProperties.ICON_COLOR;
                }

                @Override
                public Object getBean() {
                    return StackedFontIcon.this;
                }

                @Override
                public String getName() {
                    return "iconColor";
                }
            };
            iconColor.addListener(iconColorChangeListener);
        }
        return iconColor;
    }

    public void setIconSize(int size) {
        if (size <= 0) {
            throw new IllegalStateException("Argument 'size' must be greater than zero.");
        }
        iconSizeProperty().set(size);
    }

    public int getIconSize() {
        return iconSizeProperty().get();
    }

    public void setIconColor(Paint paint) {
        requireNonNull(paint, "Argument 'paint' must not be null");
        iconColorProperty().set(paint);
    }

    public Paint getIconColor() {
        return iconColorProperty().get();
    }

    public void setIconCodes(Ikon... iconCodes) {
        getChildren().clear();
        initializeSizesIfNeeded(iconCodes);
        updateIconCodes(iconCodes);
    }

    public void setIconCodeLiterals(String... iconCodes) {
        getChildren().clear();
        Ikon[] codes = new Ikon[iconCodes.length];
        for (int i = 0; i < iconCodes.length; i++) {
            codes[i] = IkonResolver.getInstance().resolveIkonHandler(iconCodes[i]).resolve(iconCodes[i]);
        }
        initializeSizesIfNeeded(iconCodes);
        updateIconCodes(codes);
    }

    /**
     * Sets the size for each child icon relative to this icon's size.
     *
     * @param iconSizes values must be within the range [0..1]
     */
    public void setIconSizes(double... iconSizes) {
        this.iconSizes = iconSizes;
        setIconSizeOnChildren(getIconSize());
    }

    public void setColors(Paint... iconColors) {
        int i = 0;
        for (Node node : getChildren()) {
            if (node instanceof Icon) {
                ((Icon) node).setIconColor(iconColors[i++]);
            }
        }
    }

    private void initializeSizesIfNeeded(Object[] array) {
        if (iconSizes.length == 0 || iconSizes.length != array.length) {
            iconSizes = new double[array.length];
            Arrays.fill(iconSizes, 1d);
        }
    }

    private void updateIconCodes(Ikon[] iconCodes) {
        for (int index = 0; index < iconCodes.length; index++) {
            getChildren().add(createFontIcon(iconCodes[index], index));
        }
    }

    private FontIcon createFontIcon(Ikon iconCode, int index) {
        FontIcon icon = new FontIcon(iconCode);
        icon.setIconSize(getIconSize());
        icon.setIconColor(getIconColor());
        int size = icon.getIconSize();
        applySizeToIcon(size, icon, index);
        return icon;
    }

    private static class StyleableProperties {
        private static final CssMetaData<StackedFontIcon, Number> ICON_SIZE =
            new CssMetaData<StackedFontIcon, Number>("-fx-icon-size",
                SizeConverter.getInstance(), 16.0) {

                @Override
                public boolean isSettable(StackedFontIcon fontIcon) {
                    return true;
                }

                @Override
                public StyleableProperty<Number> getStyleableProperty(StackedFontIcon icon) {
                    return (StyleableProperty<Number>) icon.iconSizeProperty();
                }
            };

        private static final CssMetaData<StackedFontIcon, Paint> ICON_COLOR =
            new CssMetaData<StackedFontIcon, Paint>("-fx-icon-color",
                PaintConverter.getInstance(), Color.BLACK) {

                @Override
                public boolean isSettable(StackedFontIcon node) {
                    return true;
                }

                @Override
                public StyleableProperty<Paint> getStyleableProperty(StackedFontIcon icon) {
                    return (StyleableProperty<Paint>) icon.iconColorProperty();
                }
            };

        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;

        static {
            final List<CssMetaData<? extends Styleable, ?>> styleables =
                new ArrayList<CssMetaData<? extends Styleable, ?>>(StackPane.getClassCssMetaData());
            styleables.add(ICON_SIZE);
            styleables.add(ICON_COLOR);
            STYLEABLES = unmodifiableList(styleables);
        }
    }

    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
        return StyleableProperties.STYLEABLES;
    }

    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
        return StackedFontIcon.getClassCssMetaData();
    }

    private void setIconSizeOnChildren(int size) {
        int i = 0;
        for (Node node : getChildren()) {
            if (node instanceof Icon) {
                applySizeToIcon(size, (Icon) node, i++);
            }
        }
    }

    private void applySizeToIcon(int size, Icon icon, int index) {
        double childPercentageSize = iconSizes[index];
        double newSize = size * childPercentageSize;
        icon.setIconSize((int) newSize);
    }

    private void setIconColorOnChildren(Paint color) {
        for (Node node : getChildren()) {
            if (node instanceof Icon) {
                ((Icon) node).setIconColor(color);
            }
        }
    }
}