/**
 * Copyright (c) 2010-2023 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.core.ui.internal.components;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.emf.common.util.EList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.model.core.EventType;
import org.openhab.core.model.core.ModelRepositoryChangeListener;
import org.openhab.core.model.sitemap.SitemapProvider;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.LinkableWidget;
import org.openhab.core.model.sitemap.sitemap.Mapping;
import org.openhab.core.model.sitemap.sitemap.Sitemap;
import org.openhab.core.model.sitemap.sitemap.SitemapFactory;
import org.openhab.core.model.sitemap.sitemap.SitemapPackage;
import org.openhab.core.model.sitemap.sitemap.VisibilityRule;
import org.openhab.core.model.sitemap.sitemap.Widget;
import org.openhab.core.model.sitemap.sitemap.impl.ChartImpl;
import org.openhab.core.model.sitemap.sitemap.impl.ColorArrayImpl;
import org.openhab.core.model.sitemap.sitemap.impl.ColorpickerImpl;
import org.openhab.core.model.sitemap.sitemap.impl.DefaultImpl;
import org.openhab.core.model.sitemap.sitemap.impl.FrameImpl;
import org.openhab.core.model.sitemap.sitemap.impl.GroupImpl;
import org.openhab.core.model.sitemap.sitemap.impl.ImageImpl;
import org.openhab.core.model.sitemap.sitemap.impl.InputImpl;
import org.openhab.core.model.sitemap.sitemap.impl.MappingImpl;
import org.openhab.core.model.sitemap.sitemap.impl.MapviewImpl;
import org.openhab.core.model.sitemap.sitemap.impl.SelectionImpl;
import org.openhab.core.model.sitemap.sitemap.impl.SetpointImpl;
import org.openhab.core.model.sitemap.sitemap.impl.SitemapImpl;
import org.openhab.core.model.sitemap.sitemap.impl.SliderImpl;
import org.openhab.core.model.sitemap.sitemap.impl.SwitchImpl;
import org.openhab.core.model.sitemap.sitemap.impl.TextImpl;
import org.openhab.core.model.sitemap.sitemap.impl.VideoImpl;
import org.openhab.core.model.sitemap.sitemap.impl.VisibilityRuleImpl;
import org.openhab.core.model.sitemap.sitemap.impl.WebviewImpl;
import org.openhab.core.model.sitemap.sitemap.impl.WidgetImpl;
import org.openhab.core.ui.components.RootUIComponent;
import org.openhab.core.ui.components.UIComponent;
import org.openhab.core.ui.components.UIComponentRegistry;
import org.openhab.core.ui.components.UIComponentRegistryFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This {@link SitemapProvider} provides sitemaps from all well-formed {@link RootUIComponent} found in a specific
 * "system:sitemap" namespace.
 *
 * @author Yannick Schaus - Initial contribution
 * @author Laurent Garnier - icon color support for all widgets
 */
@NonNullByDefault
@Component(service = SitemapProvider.class)
public class UIComponentSitemapProvider implements SitemapProvider, RegistryChangeListener<RootUIComponent> {
    private final Logger logger = LoggerFactory.getLogger(UIComponentSitemapProvider.class);

    public static final String SITEMAP_NAMESPACE = "system:sitemap";

    private static final String SITEMAP_PREFIX = "uicomponents_";
    private static final String SITEMAP_SUFFIX = ".sitemap";

    private static final Pattern VISIBILITY_PATTERN = Pattern
            .compile("(?<item>[A-Za-z]\\w*)\\s*(?<condition>==|!=|<=|>=|<|>)\\s*(?<sign>\\+|-)?(?<state>.+)");
    private static final Pattern COLOR_PATTERN = Pattern.compile(
            "((?<item>[A-Za-z]\\w*)?\\s*((?<condition>==|!=|<=|>=|<|>)\\s*(?<sign>\\+|-)?(?<state>[^=]+))?\\s*=)?\\s*(?<arg>\\S+)");

    private Map<String, Sitemap> sitemaps = new HashMap<>();
    private @Nullable UIComponentRegistryFactory componentRegistryFactory;
    private @Nullable UIComponentRegistry sitemapComponentRegistry;

    private final Set<ModelRepositoryChangeListener> modelChangeListeners = new CopyOnWriteArraySet<>();

    @Override
    public @Nullable Sitemap getSitemap(String sitemapName) {
        buildSitemap(sitemapName.replaceFirst(SITEMAP_PREFIX, ""));
        return sitemaps.get(sitemapName);
    }

    @Override
    public Set<String> getSitemapNames() {
        UIComponentRegistry registry = sitemapComponentRegistry;
        if (registry == null) {
            return Collections.emptySet();
        }

        sitemaps.clear();
        Collection<RootUIComponent> rootComponents = registry.getAll();
        // try building all sitemaps to leave the invalid ones out
        for (RootUIComponent rootComponent : rootComponents) {
            try {
                Sitemap sitemap = buildSitemap(rootComponent);
                sitemaps.put(sitemap.getName(), sitemap);
            } catch (Exception e) {
                logger.error("Cannot build sitemap {}", rootComponent.getUID(), e);
            }
        }

        return sitemaps.keySet();
    }

    protected @Nullable Sitemap buildSitemap(String sitemapName) {
        UIComponentRegistry registry = sitemapComponentRegistry;
        if (registry == null) {
            return null;
        }

        RootUIComponent rootComponent = registry.get(sitemapName);
        if (rootComponent != null) {
            try {
                Sitemap sitemap = buildSitemap(rootComponent);
                sitemaps.put(sitemap.getName(), sitemap);
                return null;
            } catch (Exception e) {
                logger.error("Cannot build sitemap {}", rootComponent.getUID(), e);
            }
        }

        return null;
    }

    protected Sitemap buildSitemap(RootUIComponent rootComponent) {
        if (!"Sitemap".equals(rootComponent.getType())) {
            throw new IllegalArgumentException("Root component type is not Sitemap");
        }

        SitemapImpl sitemap = (SitemapImpl) SitemapFactory.eINSTANCE.createSitemap();
        sitemap.setName(SITEMAP_PREFIX + rootComponent.getUID());
        sitemap.setLabel(rootComponent.getConfig().get("label").toString());

        if (rootComponent.getSlots() != null && rootComponent.getSlots().containsKey("widgets")) {
            for (UIComponent component : rootComponent.getSlot("widgets")) {
                Widget widget = buildWidget(component);
                if (widget != null) {
                    sitemap.getChildren().add(widget);
                }
            }
        }

        return sitemap;
    }

    protected @Nullable Widget buildWidget(UIComponent component) {
        Widget widget = null;

        switch (component.getType()) {
            case "Frame":
                FrameImpl frameWidget = (FrameImpl) SitemapFactory.eINSTANCE.createFrame();
                widget = frameWidget;
                break;
            case "Text":
                TextImpl textWidget = (TextImpl) SitemapFactory.eINSTANCE.createText();
                widget = textWidget;
                break;
            case "Group":
                GroupImpl groupWidget = (GroupImpl) SitemapFactory.eINSTANCE.createGroup();
                widget = groupWidget;
                break;
            case "Image":
                ImageImpl imageWidget = (ImageImpl) SitemapFactory.eINSTANCE.createImage();
                widget = imageWidget;
                setWidgetPropertyFromComponentConfig(widget, component, "url", SitemapPackage.IMAGE__URL);
                setWidgetPropertyFromComponentConfig(widget, component, "refresh", SitemapPackage.IMAGE__REFRESH);
                break;
            case "Video":
                VideoImpl videoWidget = (VideoImpl) SitemapFactory.eINSTANCE.createVideo();
                widget = videoWidget;
                setWidgetPropertyFromComponentConfig(widget, component, "url", SitemapPackage.VIDEO__URL);
                setWidgetPropertyFromComponentConfig(widget, component, "encoding", SitemapPackage.VIDEO__ENCODING);
                break;
            case "Chart":
                ChartImpl chartWidget = (ChartImpl) SitemapFactory.eINSTANCE.createChart();
                widget = chartWidget;
                setWidgetPropertyFromComponentConfig(widget, component, "service", SitemapPackage.CHART__SERVICE);
                setWidgetPropertyFromComponentConfig(widget, component, "refresh", SitemapPackage.CHART__REFRESH);
                setWidgetPropertyFromComponentConfig(widget, component, "period", SitemapPackage.CHART__PERIOD);
                setWidgetPropertyFromComponentConfig(widget, component, "legend", SitemapPackage.CHART__LEGEND);
                setWidgetPropertyFromComponentConfig(widget, component, "forceAsItem",
                        SitemapPackage.CHART__FORCE_AS_ITEM);
                break;
            case "Webview":
                WebviewImpl webviewWidget = (WebviewImpl) SitemapFactory.eINSTANCE.createWebview();
                widget = webviewWidget;
                setWidgetPropertyFromComponentConfig(widget, component, "height", SitemapPackage.WEBVIEW__HEIGHT);
                setWidgetPropertyFromComponentConfig(widget, component, "url", SitemapPackage.WEBVIEW__URL);
                break;
            case "Switch":
                SwitchImpl switchWidget = (SwitchImpl) SitemapFactory.eINSTANCE.createSwitch();
                addWidgetMappings(switchWidget.getMappings(), component);
                widget = switchWidget;
                break;
            case "Mapview":
                MapviewImpl mapviewWidget = (MapviewImpl) SitemapFactory.eINSTANCE.createMapview();
                widget = mapviewWidget;
                setWidgetPropertyFromComponentConfig(widget, component, "height", SitemapPackage.WEBVIEW__HEIGHT);
                break;
            case "Slider":
                SliderImpl sliderWidget = (SliderImpl) SitemapFactory.eINSTANCE.createSlider();
                widget = sliderWidget;
                setWidgetPropertyFromComponentConfig(widget, component, "minValue", SitemapPackage.SLIDER__MIN_VALUE);
                setWidgetPropertyFromComponentConfig(widget, component, "maxValue", SitemapPackage.SLIDER__MAX_VALUE);
                setWidgetPropertyFromComponentConfig(widget, component, "step", SitemapPackage.SLIDER__STEP);
                setWidgetPropertyFromComponentConfig(widget, component, "switchEnabled",
                        SitemapPackage.SLIDER__SWITCH_ENABLED);
                setWidgetPropertyFromComponentConfig(widget, component, "sendFrequency",
                        SitemapPackage.SLIDER__FREQUENCY);
                break;
            case "Selection":
                SelectionImpl selectionWidget = (SelectionImpl) SitemapFactory.eINSTANCE.createSelection();
                addWidgetMappings(selectionWidget.getMappings(), component);
                widget = selectionWidget;
                break;
            case "Input":
                InputImpl inputWidget = (InputImpl) SitemapFactory.eINSTANCE.createInput();
                widget = inputWidget;
                setWidgetPropertyFromComponentConfig(widget, component, "inputHint", SitemapPackage.INPUT__INPUT_HINT);
                break;
            case "Setpoint":
                SetpointImpl setpointWidget = (SetpointImpl) SitemapFactory.eINSTANCE.createSetpoint();
                widget = setpointWidget;
                setWidgetPropertyFromComponentConfig(widget, component, "minValue", SitemapPackage.SETPOINT__MIN_VALUE);
                setWidgetPropertyFromComponentConfig(widget, component, "maxValue", SitemapPackage.SETPOINT__MAX_VALUE);
                setWidgetPropertyFromComponentConfig(widget, component, "step", SitemapPackage.SETPOINT__STEP);
                break;
            case "Colorpicker":
                ColorpickerImpl colorpickerWidget = (ColorpickerImpl) SitemapFactory.eINSTANCE.createColorpicker();
                widget = colorpickerWidget;
                setWidgetPropertyFromComponentConfig(widget, component, "frequency",
                        SitemapPackage.COLORPICKER__FREQUENCY);
                break;
            case "Default":
                DefaultImpl defaultWidget = (DefaultImpl) SitemapFactory.eINSTANCE.createDefault();
                widget = defaultWidget;
                setWidgetPropertyFromComponentConfig(widget, component, "height", SitemapPackage.DEFAULT__HEIGHT);
                break;
            default:
                logger.warn("Unknown sitemap component type {}", component.getType());
                break;
        }

        if (widget != null) {
            setWidgetPropertyFromComponentConfig(widget, component, "label", SitemapPackage.WIDGET__LABEL);
            setWidgetPropertyFromComponentConfig(widget, component, "icon", SitemapPackage.WIDGET__ICON);
            setWidgetPropertyFromComponentConfig(widget, component, "staticIcon", SitemapPackage.WIDGET__STATIC_ICON);
            setWidgetPropertyFromComponentConfig(widget, component, "item", SitemapPackage.WIDGET__ITEM);

            if (widget instanceof LinkableWidget linkableWidget) {
                if (component.getSlots() != null && component.getSlots().containsKey("widgets")) {
                    for (UIComponent childComponent : component.getSlot("widgets")) {
                        Widget childWidget = buildWidget(childComponent);
                        if (childWidget != null) {
                            linkableWidget.getChildren().add(childWidget);
                        }
                    }
                }
            }

            addWidgetVisibility(widget.getVisibility(), component);
            addLabelColor(widget.getLabelColor(), component);
            addValueColor(widget.getValueColor(), component);
            addIconColor(widget.getIconColor(), component);
        }

        return widget;
    }

    private void setWidgetPropertyFromComponentConfig(Widget widget, @Nullable UIComponent component,
            String configParamName, int feature) {
        if (component == null || component.getConfig() == null) {
            return;
        }
        Object value = component.getConfig().get(configParamName);
        if (value == null) {
            return;
        }
        try {
            WidgetImpl widgetImpl = (WidgetImpl) widget;
            Object normalizedValue = ConfigUtil.normalizeType(value);
            if (widgetImpl.eGet(feature, false, false) instanceof Integer) {
                normalizedValue = (normalizedValue instanceof BigDecimal bd) ? bd.intValue()
                        : Integer.valueOf(normalizedValue.toString());
            } else if (widgetImpl.eGet(feature, false, false) instanceof Boolean
                    && !(normalizedValue instanceof Boolean)) {
                normalizedValue = Boolean.valueOf(normalizedValue.toString());
            }
            widgetImpl.eSet(feature, normalizedValue);
        } catch (Exception e) {
            logger.warn("Cannot set {} parameter for {} widget parameter: {}", configParamName, component.getType(),
                    e.getMessage());
        }
    }

    private void addWidgetMappings(EList<Mapping> mappings, UIComponent component) {
        if (component.getConfig() != null && component.getConfig().containsKey("mappings")) {
            if (component.getConfig().get("mappings") instanceof Collection<?>) {
                for (Object sourceMapping : (Collection<?>) component.getConfig().get("mappings")) {
                    if (sourceMapping instanceof String) {
                        String cmd = sourceMapping.toString().split("=")[0].trim();
                        String label = sourceMapping.toString().split("=")[1].trim();
                        MappingImpl mapping = (MappingImpl) SitemapFactory.eINSTANCE.createMapping();
                        mapping.setCmd(cmd);
                        mapping.setLabel(label);
                        mappings.add(mapping);
                    }
                }
            }
        }
    }

    private void addWidgetVisibility(EList<VisibilityRule> visibility, UIComponent component) {
        if (component.getConfig() != null && component.getConfig().containsKey("visibility")) {
            for (Object sourceVisibility : (Collection<?>) component.getConfig().get("visibility")) {
                if (sourceVisibility instanceof String) {
                    Matcher matcher = VISIBILITY_PATTERN.matcher(sourceVisibility.toString());
                    if (matcher.matches()) {
                        VisibilityRuleImpl visibilityRule = (VisibilityRuleImpl) SitemapFactory.eINSTANCE
                                .createVisibilityRule();
                        visibilityRule.setItem(matcher.group("item"));
                        visibilityRule.setCondition(matcher.group("condition"));
                        visibilityRule.setSign(matcher.group("sign"));
                        visibilityRule.setState(matcher.group("state"));
                        visibility.add(visibilityRule);
                    } else {
                        logger.warn("Syntax error in visibility rule '{}' for widget {}", sourceVisibility,
                                component.getType());
                    }
                }
            }
        }
    }

    private void addLabelColor(EList<ColorArray> labelColor, UIComponent component) {
        addColor(labelColor, component, "labelcolor");
    }

    private void addValueColor(EList<ColorArray> valueColor, UIComponent component) {
        addColor(valueColor, component, "valuecolor");
    }

    private void addIconColor(EList<ColorArray> iconColor, UIComponent component) {
        addColor(iconColor, component, "iconcolor");
    }

    private void addColor(EList<ColorArray> color, UIComponent component, String key) {
        if (component.getConfig() != null && component.getConfig().containsKey(key)) {
            for (Object sourceColor : (Collection<?>) component.getConfig().get(key)) {
                if (sourceColor instanceof String) {
                    Matcher matcher = COLOR_PATTERN.matcher(sourceColor.toString());
                    if (matcher.matches()) {
                        ColorArrayImpl colorArray = (ColorArrayImpl) SitemapFactory.eINSTANCE.createColorArray();
                        colorArray.setItem(matcher.group("item"));
                        colorArray.setCondition(matcher.group("condition"));
                        colorArray.setSign(matcher.group("sign"));
                        colorArray.setState(matcher.group("state"));
                        colorArray.setArg(matcher.group("arg"));
                        color.add(colorArray);
                    } else {
                        logger.warn("Syntax error in {} rule '{}' for widget {}", key, sourceColor,
                                component.getType());
                    }
                }
            }
        }
    }

    @Override
    public void addModelChangeListener(ModelRepositoryChangeListener listener) {
        modelChangeListeners.add(listener);
    }

    @Override
    public void removeModelChangeListener(ModelRepositoryChangeListener listener) {
        modelChangeListeners.remove(listener);
    }

    @Override
    public void added(RootUIComponent element) {
        for (ModelRepositoryChangeListener listener : modelChangeListeners) {
            listener.modelChanged(SITEMAP_PREFIX + element.getUID() + SITEMAP_SUFFIX, EventType.ADDED);
        }
    }

    @Override
    public void removed(RootUIComponent element) {
        for (ModelRepositoryChangeListener listener : modelChangeListeners) {
            listener.modelChanged(SITEMAP_PREFIX + element.getUID() + SITEMAP_SUFFIX, EventType.REMOVED);
        }
    }

    @Override
    public void updated(RootUIComponent oldElement, RootUIComponent element) {
        for (ModelRepositoryChangeListener listener : modelChangeListeners) {
            listener.modelChanged(SITEMAP_PREFIX + element.getUID() + SITEMAP_SUFFIX, EventType.MODIFIED);
        }
    }

    @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
    protected void setComponentRegistryFactory(UIComponentRegistryFactory componentRegistryFactory) {
        this.componentRegistryFactory = componentRegistryFactory;
        this.sitemapComponentRegistry = this.componentRegistryFactory.getRegistry(SITEMAP_NAMESPACE);
        this.sitemapComponentRegistry.addRegistryChangeListener(this);
    }

    protected void unsetComponentRegistryFactory(UIComponentRegistryFactory componentRegistryFactory) {
        UIComponentRegistry registry = this.sitemapComponentRegistry;
        if (registry != null) {
            registry.removeRegistryChangeListener(this);
        }

        this.componentRegistryFactory = null;
        this.sitemapComponentRegistry = null;
    }
}
