# flake8: noqa: E117, E127, E128, E222, E225, E231, E252, E261, E271, E272, E303, E501
# -*- coding: utf-8 -*-
"""
/***************************************************************************
 gl2qgis library

 Helps parse a GL style for vector tiles implementation in QGIS.
                              -------------------
        begin                : 2020-05-20
        copyright            : (C) 2020 by MapTiler AG.
        author               : MapTiler Team
        credits              : Martin Dobias and his GL style parser (MIT)
 ***************************************************************************/
"""

import enum
import re
import os

from qgis.PyQt.QtCore import Qt, QPointF, QSize, QSizeF, QRegularExpression
from qgis.PyQt.QtGui import QFont, QFontDatabase, QColor, QImage
from qgis.core import (
    Qgis,
    QgsBlurEffect,
    qgsDoubleNear,
    QgsEffectStack,
    QgsExpression,
    QgsFontUtils,
    QgsLabeling,
    QgsMapBoxGlStyleConversionContext,
    QgsMapBoxGlStyleConverter,
    QgsPalLayerSettings,
    QgsProperty,
    QgsPropertyCollection,
    QgsRasterFillSymbolLayer,
    QgsSingleSymbolRenderer,
    QgsSymbol,
    QgsSymbolLayer,
    QgsSymbolLayerUtils,
    QgsTextBackgroundSettings,
    QgsTextFormat,
    QgsVectorTileBasicLabeling,
    QgsVectorTileBasicLabelingStyle,
    QgsVectorTileBasicRenderer,
    QgsVectorTileBasicRendererStyle,
    QgsWkbTypes,
)
from .. import utils
from itertools import repeat
from pathlib import Path


class PropertyType(enum.Enum):
    Color = 1
    Line = 2
    Opacity = 3
    Text = 4
    Numeric = 5
    Point = 6


def parse_layers(
    source_name: str,
    style_json_data: dict,
    context: QgsMapBoxGlStyleConversionContext,
):
    """Parse list of layers from JSON and return QgsVectorTileBasicRenderer + QgsVectorTileBasicLabeling in a tuple"""

    # Sprites
    if style_json_data.get("sprite"):
        sprite_json_dict, sprite_img = get_sprites_from_style_json(
            style_json_data)
        context.setSprites(sprite_img, sprite_json_dict)

    # Parse layers
    renderer_styles = []
    labeling_styles = []
    layers = style_json_data.get("layers")
    source_layers = []
    map_id = style_json_data.get("id")
    for layer in layers:
        if "source" not in layer or layer["source"] != source_name:
            continue
        source_layers.append(layer)

    for json_layer in source_layers:
        layer_type = json_layer["type"]
        if layer_type == "background":
            continue
        style_id = json_layer["id"]
        context.setLayerId(style_id)
        layer_name = json_layer.get("source-layer")
        min_zoom = (int(json_layer["minzoom"])
                    if "minzoom" in json_layer else -1)
        max_zoom = (int(json_layer["maxzoom"])
                    if "maxzoom" in json_layer else -1)

        enabled = True
        json_layout = json_layer.get("layout")
        if json_layout:
            layer_visibility = json_layout.get("visibility")
            if layer_visibility == "none":
                enabled = False
                continue

        filter_expr = ""
        if "filter" in json_layer:
            filter_expr = parse_expression(json_layer["filter"], context)

        has_renderer_style = False
        has_labeling_style = False
        try:
            if layer_type == "fill":
                has_renderer_style, renderer_style = parse_fill_layer(
                    json_layer, context)
            elif layer_type == "line":
                has_renderer_style, renderer_style = parse_line_layer(
                    json_layer, context)
            elif layer_type == "symbol":
                (
                    has_renderer_style,
                    renderer_style,
                    has_labeling_style,
                    labeling_style,
                ) = parse_symbol_layer(json_layer, map_id, context)
            elif layer_type == "fill-extrusion":
                continue
            else:
                context.pushWarning(
                    f"{layer_type}: Skipping unknown layer type.")
                continue
        except (AttributeError, TypeError, BaseException) as e:
            context.pushWarning(
                f"{context.layerId()}: Could not convert expressions in layer style definition, "
                f"check Python console for details.")
            print(e)

        if has_renderer_style:
            renderer_style.setStyleName(style_id)
            renderer_style.setLayerName(layer_name)
            renderer_style.setFilterExpression(filter_expr)
            renderer_style.setMinZoomLevel(min_zoom)
            renderer_style.setMaxZoomLevel(max_zoom)
            renderer_style.setEnabled(enabled)
            renderer_styles.append(renderer_style)

        if has_labeling_style:
            labeling_style.setStyleName(style_id)
            labeling_style.setLayerName(layer_name)
            labeling_style.setFilterExpression(filter_expr)
            labeling_style.setMinZoomLevel(min_zoom)
            labeling_style.setMaxZoomLevel(max_zoom)
            labeling_style.setEnabled(enabled)
            labeling_styles.append(labeling_style)

    renderer = QgsVectorTileBasicRenderer()
    renderer.setStyles(renderer_styles)

    labeling = QgsVectorTileBasicLabeling()
    labeling.setStyles(labeling_styles)

    return renderer, labeling, context.warnings()


def parse_fill_layer(json_layer, context):
    json_paint = json_layer.get("paint")
    if json_paint is None:
        context.pushWarning(
            f"{json_layer.get('id')}: Layer has no paint property, skipping.")
        return False, None

    dd_properties = QgsPropertyCollection()
    dd_raster_properties = QgsPropertyCollection()

    symbol = QgsSymbol.defaultSymbol(QgsWkbTypes.PolygonGeometry)
    fill_symbol = symbol.symbolLayer(0)
    symbol.setOutputUnit(context.targetUnit())
    fill_symbol.setOutputUnit(context.targetUnit())

    # Fill color
    fill_color = None
    if "fill-color" in json_paint:
        json_fill_color = json_paint["fill-color"]
        if isinstance(json_fill_color, dict):
            # Use data defined property
            fill_color = "dd_props"
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyFillColor,
                parse_interpolate_color_by_zoom(json_fill_color, context),
            )
        elif isinstance(json_fill_color, list):
            # Use data defined property
            fill_color = "dd_props"
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyFillColor,
                parse_value_list(json_fill_color, PropertyType.Color, context,
                                 1, 255),
            )
        elif isinstance(json_fill_color, str):
            # Use simple string color
            fill_color = parse_color(json_fill_color, context)
            fill_symbol.setFillColor(fill_color)
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported fill-color type ({type(json_fill_color).__name__})"
            )

    # Fill outline color
    fill_outline_color = None
    if "fill-outline-color" in json_paint:
        json_fill_outline_color = json_paint.get("fill-outline-color")
        if isinstance(json_fill_outline_color, dict):
            fill_outline_color = "dd_props"
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyStrokeColor,
                parse_interpolate_color_by_zoom(json_fill_outline_color,
                                                context),
            )
        elif isinstance(json_fill_outline_color, list):
            fill_outline_color = "dd_props"
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyStrokeColor,
                parse_value_list(
                    json_fill_outline_color,
                    PropertyType.Color,
                    context,
                    1,
                    255,
                ),
            )
        elif isinstance(json_fill_outline_color, str):
            fill_outline_color = parse_color(json_fill_outline_color, context)
            fill_symbol.setStrokeColor(fill_outline_color)
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported fill-outline-color type ({type(json_fill_outline_color).__name__})."
            )

    # Fill opacity
    fill_opacity = None
    raster_opacity = None

    json_fill_opacity = json_paint.get("fill-opacity")
    if json_fill_opacity:
        if isinstance(json_fill_opacity, (float, int)):
            fill_opacity = float(json_fill_opacity)
            raster_opacity = fill_opacity
        elif isinstance(json_fill_opacity, dict) and (
                dd_properties.isActive(QgsSymbolLayer.PropertyFillColor)
                or dd_properties.isActive(QgsSymbolLayer.PropertyStrokeColor)):
            # opacity already defined in fill color
            fill_opacity = None
        elif isinstance(json_fill_opacity, dict):
            fill_opacity = None
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyFillColor,
                parse_interpolate_opacity_by_zoom(
                    json_fill_opacity,
                    (fill_color.alpha()
                     if isinstance(fill_color, QColor) else 255),
                ),
            )
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyStrokeColor,
                parse_interpolate_opacity_by_zoom(
                    json_fill_opacity,
                    (fill_outline_color.alpha() if isinstance(
                        fill_outline_color, QColor) else 255),
                ),
            )
            dd_raster_properties.setProperty(
                QgsSymbolLayer.PropertyOpacity,
                parse_interpolate_by_zoom(json_fill_opacity, context, 100),
            )
        elif isinstance(json_fill_opacity, list) and dd_properties.isActive(
                QgsSymbolLayer.PropertyFillColor):
            # opacity already defined in fill color
            fill_opacity = None
        elif isinstance(json_fill_opacity, list):
            fill_opacity = None
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyFillColor,
                parse_value_list(
                    json_fill_opacity,
                    PropertyType.Opacity,
                    context,
                    1,
                    (fill_color.alpha()
                     if isinstance(fill_color, QColor) else 255),
                ),
            )
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyStrokeColor,
                parse_value_list(
                    json_fill_opacity,
                    PropertyType.Opacity,
                    context,
                    1,
                    (fill_outline_color.alpha() if isinstance(
                        fill_outline_color, QColor) else 255),
                ),
            )
            dd_raster_properties.setProperty(
                QgsSymbolLayer.PropertyOpacity,
                parse_value_list(json_fill_opacity, PropertyType.Numeric,
                                 context, 100, 255),
            )
        else:
            fill_opacity = None
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported fill-opacity type ({type(json_fill_opacity).__name__})"
            )

    # Fill-translate
    json_fill_translate = json_paint.get("fill-translate")
    fill_translate = QPointF()
    if json_fill_translate:
        if isinstance(json_fill_translate, dict):
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyOffset,
                parse_interpolate_point_by_zoom(
                    json_fill_translate,
                    context,
                    context.pixelSizeConversionFactor(),
                ),
            )
        elif isinstance(json_fill_translate, list):
            fill_translate = QPointF(
                json_fill_translate[0] * context.pixelSizeConversionFactor(),
                json_fill_translate[1] * context.pixelSizeConversionFactor(),
            )
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported fill-translate type ({type(json_fill_translate).__name__})"
            )

    if not fill_translate.isNull():
        fill_symbol.setOffset(fill_translate)
    fill_symbol.setOffsetUnit(context.targetUnit())

    # Fill-pattern
    json_fill_patern = json_paint.get("fill-pattern")
    if json_fill_patern:
        # fill-pattern can be String or Object
        # String: {"fill-pattern": "dash-t"}
        # Object: {"fill-pattern":{"stops":[[11,"wetland8"],[12,"wetland16"]]}}

        core_converter = QgsMapBoxGlStyleConverter()
        sprite_size = QSize()
        sprite_property = ""
        sprite_size_property = ""
        if int(Qgis.QGIS_VERSION_INT) >= 34000:
            sprite = core_converter.retrieveSpriteAsBase64(
                json_fill_patern, context)
        else:
            sprite = core_converter.retrieveSpriteAsBase64(
                json_fill_patern,
                context,
                sprite_size,
                sprite_property,
                sprite_size_property,
            )

        if sprite:
            # when fill-pattern exists, set and insert QgsRasterFillSymbolLayer
            raster_fill = QgsRasterFillSymbolLayer()
            raster_fill.setImageFilePath(sprite)
            raster_fill.setWidth(sprite_size.width())
            raster_fill.setWidthUnit(context.targetUnit())
            raster_fill.setCoordinateMode(QgsRasterFillSymbolLayer.Viewport)
            fill_color = None

            if raster_opacity:
                raster_fill.setOpacity(raster_opacity)
            if sprite_property:
                dd_raster_properties.setProperty(
                    QgsSymbolLayer.PropertyFile,
                    QgsProperty.fromExpression(sprite_property),
                )
                dd_raster_properties.setProperty(
                    QgsSymbolLayer.PropertyWidth,
                    QgsProperty.fromExpression(sprite_size_property),
                )

            raster_fill.setDataDefinedProperties(dd_raster_properties)
            symbol.appendSymbolLayer(raster_fill)
        else:
            fill_color = None
            fill_outline_color = None

    fill_symbol.setDataDefinedProperties(dd_properties)

    if fill_opacity:
        symbol.setOpacity(fill_opacity)

    if fill_outline_color == "dd_props":
        fill_symbol.setStrokeStyle(Qt.PenStyle(Qt.PenStyle.SolidLine))
    elif fill_outline_color:
        fill_symbol.setStrokeColor(fill_outline_color)
    elif fill_outline_color is None:
        fill_symbol.setStrokeStyle(Qt.PenStyle(Qt.PenStyle.NoPen))

    if fill_color == "dd_props":
        fill_symbol.setBrushStyle(Qt.BrushStyle(Qt.BrushStyle.SolidPattern))
    elif fill_color:
        fill_symbol.setFillColor(fill_color)
    elif fill_color is None:
        fill_symbol.setBrushStyle(Qt.BrushStyle(Qt.BrushStyle.NoBrush))

    style = QgsVectorTileBasicRendererStyle()
    style.setGeometryType(QgsWkbTypes.PolygonGeometry)
    style.setSymbol(symbol)

    return True, style


def parse_line_layer(json_layer: dict,
                     context: QgsMapBoxGlStyleConversionContext):
    json_paint = json_layer.get("paint")
    if json_paint is None:
        context.pushWarning(
            f"{json_layer.get('id')}: Layer has no paint property, skipping.")
        return False, None

    if "line-pattern" in json_paint:
        context.pushWarning(
            f"{context.layerId()}: Skipping unsupported line-pattern property."
        )
        return False, None

    # Default
    symbol = QgsSymbol.defaultSymbol(QgsWkbTypes.LineGeometry)
    line_symbol = symbol.symbolLayer(0)
    symbol.setOutputUnit(context.targetUnit())
    line_symbol.setOutputUnit(context.targetUnit())
    dd_properties = QgsPropertyCollection()

    # Line color
    line_color = None
    json_line_color = json_paint.get("line-color")
    if json_line_color:
        if isinstance(json_line_color, dict):
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyFillColor,
                parse_interpolate_color_by_zoom(json_line_color, context),
            )
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyStrokeColor,
                dd_properties.property(QgsSymbolLayer.PropertyFillColor),
            )
        elif isinstance(json_line_color, list):
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyFillColor,
                parse_value_list(json_line_color, PropertyType.Color, context,
                                 1, 255),
            )
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyStrokeColor,
                dd_properties.property(QgsSymbolLayer.PropertyFillColor),
            )
        elif isinstance(json_line_color, str):
            line_color = parse_color(json_line_color, context)
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported line-color type ({type(json_line_color).__name__})."
            )
    else:
        # Default to #000000
        line_color = QColor(0, 0, 0)

    # Line width
    line_width = line_symbol.width()
    line_width_property = None
    json_line_width = json_paint.get("line-width")
    if json_line_width:
        if isinstance(json_line_width, (float, int)):
            line_width = float(json_line_width *
                               context.pixelSizeConversionFactor())
        elif isinstance(json_line_width, dict):
            line_width = None
            line_width_property = parse_interpolate_by_zoom(
                json_line_width, context, context.pixelSizeConversionFactor())
            dd_properties.setProperty(QgsSymbolLayer.PropertyStrokeWidth,
                                      line_width_property)
        elif isinstance(json_line_width, list):
            line_width_property = parse_value_list(
                json_line_width,
                PropertyType.Numeric,
                context,
                context.pixelSizeConversionFactor(),
                255,
            )
            dd_properties.setProperty(QgsSymbolLayer.PropertyStrokeWidth,
                                      line_width_property)
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported fill-width type ({type(json_line_width).__name__})."
            )

    # Line offset
    line_offset = 0.0
    json_line_offset = json_paint.get("line-offset")
    if json_line_offset:
        if isinstance(json_line_offset, (int, float)):
            line_offset = (-float(json_line_offset) *
                           context.pixelSizeConversionFactor())
        elif isinstance(json_line_offset, dict):
            line_offset = None
            line_width = None
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyOffset,
                parse_interpolate_by_zoom(
                    json_line_offset,
                    context,
                    context.pixelSizeConversionFactor() * -1,
                ),
            )
        elif isinstance(json_line_offset, list):
            line_offset = None
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyOffset,
                parse_value_list(
                    json_line_offset,
                    PropertyType.Numeric,
                    context,
                    context.pixelSizeConversionFactor() * -1,
                    255,
                ),
            )
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported line-offset type ({type(json_line_offset).__name__})."
            )

    # Line opacity
    line_opacity = None
    json_line_opacity = json_paint.get("line-opacity")
    if json_line_opacity:
        if isinstance(json_line_opacity, (float, int)):
            line_opacity = float(json_line_opacity)
        elif isinstance(json_line_opacity, dict) and dd_properties.isActive(
                QgsSymbolLayer.PropertyStrokeColor):
            # opacity already defined in stroke color
            line_opacity = None
        elif isinstance(json_line_opacity, dict):
            line_opacity = None
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyStrokeColor,
                parse_interpolate_opacity_by_zoom(
                    json_line_opacity,
                    (line_color.alpha()
                     if isinstance(line_color, QColor) else 255),
                ),
            )
        elif isinstance(json_line_opacity, list) and dd_properties.isActive(
                QgsSymbolLayer.PropertyStrokeColor):
            # opacity already defined in stroke color
            line_opacity = None
        elif isinstance(json_line_opacity, list):
            line_opacity = None
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyFillColor,
                parse_value_list(
                    json_line_opacity,
                    PropertyType.Opacity,
                    context,
                    1,
                    (line_color.alpha()
                     if isinstance(line_color, QColor) else 255),
                ),
            )
        else:
            line_opacity = None
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported line-opacity type ({type(json_line_opacity).__name__})."
            )

    # Line dash
    dash_vector = None
    json_dasharray = json_paint.get("line-dasharray")
    if json_dasharray:
        if isinstance(json_dasharray, dict):
            stops = json_dasharray.get("stops")
            if line_width_property:
                expr = (
                    f"array_to_string("
                    f"array_foreach({parse_array_stops(stops, 1)},"
                    f"@element * coalesce("
                    f"({line_width_property.asExpression()}), "
                    f"{line_symbol.width()} * {context.pixelSizeConversionFactor()})), ';')"
                )
            else:
                expr = f"array_to_string({parse_array_stops(stops, 1)}, ';')"
            dd_properties.setProperty(
                QgsSymbolLayer.PropertyCustomDash,
                QgsProperty.fromExpression(expr),
            )
            dash_source = stops[-1][1]
            if line_width:
                dash_vector = [i * line_width for i in dash_source]
            else:
                dash_vector = dash_source
        elif isinstance(json_dasharray, list):
            if line_width_property:
                expr = (
                    f"array_to_string("
                    f"array_foreach(array({','.join(map(str,json_dasharray))}),"
                    f"@element * coalesce("
                    f"({line_width_property.asExpression()}), "
                    f"{line_symbol.width()} * {context.pixelSizeConversionFactor()})), ';')"
                )
                dd_properties.setProperty(
                    QgsSymbolLayer.PropertyCustomDash,
                    QgsProperty.fromExpression(expr),
                )
            if line_width:
                dash_vector = [i * line_width for i in json_dasharray]
            else:
                dash_vector = json_dasharray

        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported line-dasharray type"
                f" ({type(json_dasharray).__name__}).")

    # Layout
    pen_cap_style = Qt.PenCapStyle.FlatCap
    pen_join_style = Qt.PenJoinStyle.MiterJoin

    json_layout = json_layer.get("layout")
    if json_layout:
        if "line-cap" in json_layout:
            pen_cap_style = parse_cap_style(json_layout.get("line-cap"))
        if "line-join" in json_layout:
            pen_join_style = parse_join_style(json_layout.get("line-join"))

    line_symbol.setPenCapStyle(pen_cap_style)
    line_symbol.setPenJoinStyle(pen_join_style)
    line_symbol.setDataDefinedProperties(dd_properties)
    if line_offset:
        line_symbol.setOffset(line_offset)
    line_symbol.setOffsetUnit(context.targetUnit())

    if dash_vector:
        line_symbol.setCustomDashVector(dash_vector)
        line_symbol.setUseCustomDashPattern(True)
        line_symbol.setStrokeColor(QColor("transparent"))
    if line_color:
        line_symbol.setColor(line_color)
    if line_opacity:
        symbol.setOpacity(line_opacity)
    if line_width:
        line_symbol.setWidth(line_width)

    style = QgsVectorTileBasicRendererStyle()
    style.setGeometryType(QgsWkbTypes.LineGeometry)
    style.setSymbol(symbol)

    return True, style


def parse_symbol_layer(json_layer: dict, map_id: str,
                       context: QgsMapBoxGlStyleConversionContext):
    has_labeling = False
    has_renderer = False
    core_converter = QgsMapBoxGlStyleConverter()

    json_layout = json_layer.get("layout")
    if not json_layout:
        context.pushWarning(
            f"{json_layer.get('id')}: Layer has no layout property, skipping.")
        return has_renderer, None, has_labeling, None

    if "text-field" not in json_layout:
        has_renderer, renderer_style = (
            core_converter.parseSymbolLayerAsRenderer(json_layer, context))
        return has_renderer, renderer_style, has_labeling, None

    json_paint = json_layer.get("paint")
    if json_paint is None:
        context.pushWarning(
            f"{json_layer.get('id')}: Layer has no paint property, skipping.")
        return has_renderer, None, has_labeling, None

    dd_label_properties = QgsPropertyCollection()

    text_size = 16 * context.pixelSizeConversionFactor()
    text_size_property = QgsProperty()
    json_text_size = json_layout.get("text-size")
    if json_text_size:
        if isinstance(json_text_size, (float, int)):
            text_size = json_text_size * context.pixelSizeConversionFactor()
        elif isinstance(json_text_size, dict):
            text_size_property = parse_interpolate_by_zoom(
                json_text_size, context, context.pixelSizeConversionFactor())
        elif isinstance(json_text_size, list):
            text_size_property = parse_value_list(
                json_text_size,
                PropertyType.Numeric,
                context,
                context.pixelSizeConversionFactor(),
                255,
            )
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported text-size type ({type(json_text_size).__name__})."
            )
            return has_renderer, None, has_labeling, None

    if text_size_property:
        dd_label_properties.setProperty(QgsPalLayerSettings.Size,
                                        text_size_property)

    # a rough average of ems to character count conversion for a variety of fonts
    EM_TO_CHARS = 2.0

    # Text max width
    json_text_max_width = json_layout.get("text-max-width")
    text_max_width = None
    if json_text_max_width:
        if isinstance(json_text_max_width, (int, float)):
            text_max_width = json_text_max_width * EM_TO_CHARS
        elif isinstance(json_text_max_width, dict):
            dd_label_properties.setProperty(
                QgsPalLayerSettings.AutoWrapLength,
                parse_interpolate_by_zoom(json_text_max_width, context,
                                          EM_TO_CHARS),
            )
        elif isinstance(json_text_max_width, list):
            dd_label_properties.setProperty(
                QgsPalLayerSettings.AutoWrapLength,
                parse_value_list(
                    json_text_max_width,
                    PropertyType.Numeric,
                    context,
                    EM_TO_CHARS,
                    255,
                ),
            )
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported text-max-width type ({type(json_text_max_width).__name__})."
            )
    else:
        text_max_width = 10 * EM_TO_CHARS

    # Text letter spacing
    text_letter_spacing = None
    json_text_letter_spacing = json_layout.get("text-letter-spacing")
    if json_text_letter_spacing:
        if isinstance(json_text_letter_spacing, (int, float)):
            text_letter_spacing = float(json_text_letter_spacing)
        elif isinstance(json_text_letter_spacing, dict):
            dd_label_properties.setProperty(
                QgsPalLayerSettings.FontLetterSpacing,
                parse_interpolate_by_zoom(json_text_letter_spacing, context,
                                          1),
            )
        elif isinstance(json_text_letter_spacing, list):
            dd_label_properties.setProperty(
                QgsPalLayerSettings.FontLetterSpacing,
                parse_value_list(
                    json_text_letter_spacing,
                    PropertyType.Numeric,
                    context,
                    1,
                    255,
                ),
            )
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported text-letter-spacing type ({type(json_text_letter_spacing).__name__})."
            )

    # Text font
    text_font = QFont()

    def check_font_dbs(font_name, font_style):
        if QgsFontUtils.fontFamilyOnSystem(font_name):
            if font_style == " " or font_style is None:
                return font_name, None
            elif QgsFontUtils.fontFamilyHasStyle(font_name, font_style):
                return font_name, font_style
        elif font_name in QFontDatabase().families():
            if font_style == " ":
                return font_name, None
            elif font_style in QFontDatabase().styles(font_name):
                return f"{font_name} {font_style}", None
        return None, None

    def split_font_family(font_name):
        text_font_parts = font_name.split(" ")
        for i in range(len(text_font_parts)):
            candidate_font_name = " ".join(text_font_parts[:i + 1])
            candidate_font_style = " ".join(text_font_parts[i + 1:])
            # Check as-is
            found_name, found_style = check_font_dbs(candidate_font_name,
                                                     candidate_font_style)
            if found_name:
                return True, found_name, found_style
            # Remove gaps after "Extra" or "Semi"
            if "Extra " in candidate_font_style:
                candidate_font_style = candidate_font_style.replace(
                    "Extra ", "Extra")
            if "Semi " in candidate_font_style:
                candidate_font_style = candidate_font_style.replace(
                    "Semi ", "Semi")
            if "Semibold" in candidate_font_style:
                candidate_font_style = candidate_font_style.replace(
                    "Semibold", "SemiBold")
            # Check again with no gaps
            found_name, found_style = check_font_dbs(candidate_font_name,
                                                     candidate_font_style)
            if found_name:
                return True, found_name, found_style
        return False, None, None

    json_text_font = json_layout.get("text-font")
    font_name = None
    if json_text_font:
        if not isinstance(json_text_font, (str, list, dict)):
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported text-font type {type(json_text_font)}"
            )
        else:
            split_ok = False
            if isinstance(json_text_font, list):
                for font_name in json_text_font:
                    split_ok, font_family, font_style = split_font_family(
                        font_name)
                    if split_ok:
                        break
                if not split_ok:
                    context.pushWarning(
                        f"{context.layerId()}: None of fonts in {json_text_font} is available on "
                        f"your system. Default font will be used.")
            elif isinstance(json_text_font, str):
                split_ok, font_family, font_style = split_font_family(
                    json_text_font)
                if not split_ok:
                    context.pushWarning(
                        f"{context.layerId()}: Referenced font {json_text_font} is not available on your "
                        f"system. Default font will be used.")
            elif isinstance(json_text_font, dict):
                family_case_string = "CASE "
                style_case_string = "CASE "
                stops = json_text_font.get("stops")
                error = False
                for i in range(len(stops) - 1):
                    # Bottom zoom and value
                    bz = stops[i][0]
                    bv = (stops[i][1]
                          if isinstance(stops[i][1], str) else stops[i][1][0])
                    if isinstance(bz, list):
                        context.pushWarning(
                            f"{context.layerId()}: Expressions in interpolation function are not "
                            f"supported, skipping")
                        error = True

                    # Top zoom
                    tz = stops[i + 1][0]
                    if isinstance(tz, list):
                        context.pushWarning(
                            f"{context.layerId()}: Expressions in interpolation function are not "
                            f"supported, skipping")
                        error = True
                    split_ok, font_family, font_style = split_font_family(bv)
                    if split_ok:
                        family_case_string += (
                            f"WHEN @vector_tile_zoom > {bz} AND @vector_tile_zoom <= {tz} "
                            f"THEN {QgsExpression.quotedValue(font_family)} ")
                        style_case_string += (
                            f"WHEN @vector_tile_zoom > {bz} AND @vector_tile_zoom <= {bz} "
                            f"THEN {QgsExpression.quotedValue(font_style)} ")
                    else:
                        context.pushWarning(
                            f"{context.layerId()}: Referenced font {bv} is not available on your "
                            f"system. Default font will be used.")
                if error:
                    return False, None, False, None
                bv = (stops[-1][1]
                      if isinstance(stops[-1][1], str) else stops[-1][1][0])
                split_ok, font_family, font_style = split_font_family(bv)
                if split_ok:
                    family_case_string += (
                        f"ELSE {QgsExpression.quotedValue(font_family)} END")
                    style_case_string += (
                        f"ElSE {QgsExpression.quotedValue(font_style)} END")
                    dd_label_properties.setProperty(
                        QgsPalLayerSettings.Family,
                        QgsProperty.fromExpression(family_case_string),
                    )
                    dd_label_properties.setProperty(
                        QgsPalLayerSettings.FontStyle,
                        QgsProperty.fromExpression(style_case_string),
                    )
                else:
                    context.pushWarning(
                        f"{context.layerId()}: Referenced font {bv} is not available on your system. "
                        f"Default font will be used.")
            if split_ok:
                text_font = QFont(font_family)
                if font_style:
                    text_font.setStyleName(font_style)

    # Text color
    text_color = None
    json_text_color = json_paint.get("text-color")
    if json_text_color:
        if isinstance(json_text_color, dict):
            dd_label_properties.setProperty(
                QgsPalLayerSettings.Color,
                parse_interpolate_color_by_zoom(json_text_color, context),
            )
        elif isinstance(json_text_color, list):
            dd_label_properties.setProperty(
                QgsPalLayerSettings.Color,
                parse_value_list(json_text_color, PropertyType.Color, context,
                                 1, 255),
            )
        elif isinstance(json_text_color, str):
            text_color = parse_color(json_text_color, context)
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported text-color type ({type(json_text_color).__name__})."
            )
    else:
        # Default to #000000
        text_color = QColor(0, 0, 0)

    # Buffer color
    buffer_color = None
    json_text_halo_color = json_paint.get("text-halo-color")
    if json_text_halo_color:
        if isinstance(json_text_halo_color, dict):
            dd_label_properties.setProperty(
                QgsPalLayerSettings.BufferColor,
                parse_interpolate_color_by_zoom(json_text_halo_color, context),
            )
        elif isinstance(json_text_halo_color, list):
            dd_label_properties.setProperty(
                QgsPalLayerSettings.BufferColor,
                parse_value_list(json_text_halo_color, PropertyType.Color,
                                 context, 1, 255),
            )
        elif isinstance(json_text_halo_color, str):
            buffer_color = parse_color(json_text_halo_color, context)
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported text-halo-color type ({json_text_halo_color})."
            )

    # Buffer size
    # the pixel based text buffers appear larger when rendered on the web - so automatically scale
    # them up when converting to a QGIS style
    # (this number is based on trial-and-error comparisons only!)

    BUFFER_SIZE_SCALE = 2.0
    json_text_halo_width = json_paint.get("text-halo-width")
    buffer_size = 0
    if json_text_halo_width:
        if isinstance(json_text_halo_width, (float, int)):
            buffer_size = (json_text_halo_width *
                           context.pixelSizeConversionFactor() *
                           BUFFER_SIZE_SCALE)
        elif isinstance(json_text_halo_width, dict):
            buffer_size = 1
            dd_label_properties.setProperty(
                QgsPalLayerSettings.BufferSize,
                parse_interpolate_by_zoom(
                    json_text_halo_width,
                    context,
                    context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE,
                ),
            )
        elif isinstance(json_text_halo_width, list):
            buffer_size = 1
            dd_label_properties.setProperty(
                QgsPalLayerSettings.BufferSize,
                parse_value_list(
                    json_text_halo_width,
                    PropertyType.Numeric,
                    context,
                    context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE,
                    255,
                ),
            )
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported text-halo-width type ({type(json_text_halo_width).__name__})."
            )

    # Halo blur size
    halo_blur_size = 0
    json_text_halo_blur = json_paint.get("text-halo-blur")
    if json_text_halo_blur:
        if isinstance(json_text_halo_blur, (int, float)):
            halo_blur_size = (json_text_halo_blur *
                              context.pixelSizeConversionFactor())

    format = QgsTextFormat()
    format.setSizeUnit(context.targetUnit())
    if text_color:
        format.setColor(text_color)
    if text_size:
        format.setSize(text_size)
    if text_font:
        format.setFont(text_font)
    if text_letter_spacing:
        f = format.font()
        f.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing,
                           text_letter_spacing)
        format.setFont(f)
    if buffer_size and buffer_color and buffer_color.alpha() > 0:
        format.buffer().setEnabled(True)
        format.buffer().setSize(buffer_size)
        format.buffer().setSizeUnit(context.targetUnit())
        format.buffer().setColor(buffer_color)
        if halo_blur_size:
            stack = QgsEffectStack()
            blur = QgsBlurEffect()
            blur.setEnabled(True)
            blur.setBlurUnit(context.targetUnit())
            blur.setBlurLevel(halo_blur_size)
            blur.setBlurMethod(QgsBlurEffect.StackBlur)
            stack.appendEffect(blur)
            stack.setEnabled(True)
            format.buffer().setPaintEffect(stack)

    label_settings = QgsPalLayerSettings()
    if text_max_width:
        label_settings.autoWrapLength = int(text_max_width)

    json_text_field = json_layout.get("text-field")
    if json_text_field:
        if isinstance(json_text_field, str):
            field_name, field_is_expression = process_label_field(
                json_text_field)
            label_settings.fieldName = field_name
            label_settings.isExpression = field_is_expression
        elif isinstance(json_text_field, list):
            if len(json_text_field) > 2 and json_text_field[0] == "format":
                # e.g.
                # "text-field": ["format",
                #                "foo", { "font-scale": 1.2 },
                #                "bar", { "font-scale": 0.8 }
                # ]
                parts = []
                for i in range(len(json_text_field)):
                    is_expression = False
                    part, is_expression = process_label_field(
                        json_text_field[i])
                    if not is_expression:
                        parts.append(QgsExpression.quotedColumnRef(part))
                    else:
                        parts.append(part)
                    i += 1
                label_settings.fieldName = f"concat({','.join(parts)})"
                label_settings.isExpression = True
            else:
                # e.g.
                # "text-field": ["to-string", ["get", "name"]]
                label_settings.fieldName = parse_expression(
                    json_text_field, context)
                label_settings.isExpression = True
        elif isinstance(json_text_field, dict):
            label_settings.fieldName = parse_field_name_dict(
                json_text_field, context)
            label_settings.isExpression = True
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported text-field type ({type(json_text_field).__name__})."
            )

    # Text transform
    text_transform = json_layout.get("text-transform")
    if text_transform:
        if text_transform == "uppercase":
            label_settings.fieldName = f"upper({label_settings.fieldName if label_settings.isExpression else QgsExpression.quotedColumnRef(label_settings.fieldName)})"
        elif text_transform == "lowercase":
            label_settings.fieldName = f"lower({label_settings.fieldName if label_settings.isExpression else QgsExpression.quotedColumnRef(label_settings.fieldName)})"
        label_settings.isExpression = True

    # Placement
    label_settings.placement = Qgis.LabelPlacement.OverPoint
    geometry_type = QgsWkbTypes.PointGeometry
    symbol_placement = json_layout.get("symbol-placement")
    text_offset = None
    if symbol_placement == "line" or ("line" in str(symbol_placement)):
        label_settings.placement = Qgis.LabelPlacement.Curved
        label_settings.lineSettings().setPlacementFlags(QgsLabeling.OnLine)
        geometry_type = QgsWkbTypes.LineGeometry

        text_rotation_alignment = json_layout.get("text-rotation-alignment")
        if text_rotation_alignment:
            if text_rotation_alignment == "viewport":
                label_settings.placement = Qgis.LabelPlacement.Horizontal

        if label_settings.placement == Qgis.LabelPlacement.Curved:
            json_text_offset = json_layout.get("text-offset")
            text_offset_property = None
            if json_text_offset:
                if isinstance(json_text_offset, dict):
                    text_offset_property = parse_interpolate_point_by_zoom(
                        json_text_offset,
                        context,
                        text_size if not text_size_property else 1.0,
                    )
                    if not text_size_property:
                        dd_label_properties.setProperty(
                            QgsPalLayerSettings.LabelDistance,
                            f"abs(array_get({text_offset_property},1))-{text_size}",
                        )
                    else:
                        dd_label_properties.setProperty(
                            QgsPalLayerSettings.LabelDistance,
                            f"with_variable('text_size',{text_size_property.asExpression()},abs(array_get({text_offset_property.asExpression()},1))*@text_size-@text_size)",
                        )
                    dd_label_properties.setProperty(
                        QgsPalLayerSettings.LinePlacementOptions,
                        f"if(array_get({text_offset_property},1)>0,'BL','AL')",
                    )
                elif isinstance(json_text_offset, list):
                    text_offset = QPointF(
                        json_text_offset[0] * text_size,
                        json_text_offset[1] * text_size,
                    )
                else:
                    context.pushWarning(
                        f"{context.layerId()}: Skipping unsupported text-offset type ({type(json_text_offset).__name__})"
                    )

                if text_offset:
                    label_settings.distUnits = context.targetUnit()
                    label_settings.dist = abs(text_offset.y()) - text_size
                    label_settings.lineSettings().setPlacementFlags(
                        QgsLabeling.BelowLine if text_offset.y() >
                        0.0 else QgsLabeling.AboveLine)
                    if text_size_property and not text_offset_property:
                        dd_label_properties.setProperty(
                            QgsPalLayerSettings.LabelDistance,
                            f"with_variable('text_size',{text_size_property.asExpression()},{abs(text_offset.y()/text_size)}*@text_size-@text_size)",
                        )

            if text_offset:
                label_settings.distUnits = context.targetUnit()
                label_settings.dist = abs(text_offset.y()) - text_size
                label_settings.lineSettings().setPlacementFlags(
                    QgsLabeling.BelowLine if text_offset.y() >
                    0.0 else QgsLabeling.AboveLine)
                if text_offset and not text_offset_property:
                    dd_label_properties.setProperty(
                        QgsPalLayerSettings.LabelDistance,
                        f"with_variable('text_size',{abs(text_offset.y()/text_size)},{text_size_property.asExpression()}*@text_size-@text_size)",
                    )
            if not text_offset:
                label_settings.lineSettings().setPlacementFlags(
                    QgsLabeling.OnLine)

    json_text_justify = json_layout.get("text-justify")

    if json_text_justify:
        if isinstance(json_text_justify, str):
            text_align = json_text_justify
            if text_align == "left":
                label_settings.multilineAlign = QgsPalLayerSettings.MultiLeft
            elif text_align == "right":
                label_settings.multilineAlign = QgsPalLayerSettings.MultiRight
            elif text_align == "center":
                label_settings.multilineAlign = QgsPalLayerSettings.MultiCenter
            elif text_align == "follow":
                label_settings.multilineAlign = (
                    QgsPalLayerSettings.MultiFollowPlacement)
        else:
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported text-justify type ({type(json_text_justify).__name__})."
            )
    else:
        label_settings.multilineAlign = QgsPalLayerSettings.MultiCenter

    if label_settings.placement == Qgis.LabelPlacement.OverPoint:
        # Default
        text_anchor = "center"
        json_text_anchor = json_layout.get("text-anchor")
        if json_text_anchor:
            if isinstance(json_text_anchor, str):
                text_anchor = json_text_anchor
            elif isinstance(json_text_anchor, dict):
                stops = json_text_anchor.get("stops")
                text_anchor = stops[-1][-1]
            else:
                context.pushWarning(
                    f"{context.layerId()}: Skipping unsupported text-anchor type ({type(json_text_anchor).__name__})"
                )

            if text_anchor == "center":
                label_settings.quadOffset = QgsPalLayerSettings.QuadrantOver
            elif text_anchor == "left":
                label_settings.quadOffset = QgsPalLayerSettings.QuadrantRight
            elif text_anchor == "right":
                label_settings.quadOffset = QgsPalLayerSettings.QuadrantLeft
            elif text_anchor == "top":
                label_settings.quadOffset = QgsPalLayerSettings.QuadrantBelow
            elif text_anchor == "bottom":
                label_settings.quadOffset = QgsPalLayerSettings.QuadrantOver
            elif text_anchor == "top-left":
                label_settings.quadOffset = (
                    QgsPalLayerSettings.QuadrantBelowRight)
            elif text_anchor == "top-right":
                label_settings.quadOffset = (
                    QgsPalLayerSettings.QuadrantBelowLeft)
            elif text_anchor == "bottom-left":
                label_settings.quadOffset = QgsPalLayerSettings.QuadrantRight
            elif text_anchor == "bottom-right":
                label_settings.quadOffset = QgsPalLayerSettings.QuadrantLeft

        json_text_offset = json_layout.get("text-offset")
        if json_text_offset:
            if isinstance(json_text_offset, dict):
                dd_label_properties.setProperty(
                    QgsPalLayerSettings.OffsetXY,
                    parse_interpolate_point_by_zoom(json_text_offset, context,
                                                    text_size),
                )
            elif isinstance(json_text_offset, list):
                if json_text_offset[0] == "literal":
                    text_offset = QPointF(
                        json_text_offset[1][0] * text_size,
                        json_text_offset[1][1] * text_size,
                    )
                else:
                    text_offset = QPointF(
                        json_text_offset[0] * text_size,
                        json_text_offset[1] * text_size,
                    )
            else:
                context.pushWarning(
                    f"{context.layerId()}: Skipping unsupported text-offset type ({type(json_text_offset).__name__})"
                )

            if text_offset:
                label_settings.offsetUnits = context.targetUnit()
                label_settings.xOffset = text_offset.x()
                label_settings.yOffset = text_offset.y()

    if text_size:
        label_settings.priority = int(
            min(text_size / (context.pixelSizeConversionFactor() * 3), 10.0))

    # highway-shields
    json_icon_image = json_layout.get("icon-image")
    if (json_icon_image and label_settings.placement
            in (Qgis.LabelPlacement.Horizontal, Qgis.LabelPlacement.Curved)
            and json_layer.get("id").startswith("highway-shield")):
        backgroundSettings = QgsTextBackgroundSettings()
        backgroundSettings.setEnabled(True)
        backgroundSettings.setType(QgsTextBackgroundSettings.ShapeSVG)
        dd_label_properties.setProperty(
            QgsPalLayerSettings.ShapeSVGFile,
            parse_svg_path(json_icon_image, map_id, context),
        )
        try:
            sz_type = (QgsTextBackgroundSettings.SizeType.SizeBuffer
                       )  # Qt6 (QGIS4)
        except AttributeError:
            sz_type = 0  # backward-compatible
        backgroundSettings.setSizeType(sz_type)  # buffer
        backgroundSettings.setSizeUnit(context.targetUnit())
        backgroundSettings.setSize(QSizeF(1, 1))
        format.setBackground(backgroundSettings)
        label_settings.priority = 0

    label_settings.setFormat(format)
    label_settings.obstacleSettings().setFactor(0.1)
    label_settings.setDataDefinedProperties(dd_label_properties)

    labeling_style = QgsVectorTileBasicLabelingStyle()
    labeling_style.setGeometryType(geometry_type)
    labeling_style.setLabelSettings(label_settings)

    has_labeling_style = True

    # renderer_style, has_renderer_style = parse_symbol_layer_as_renderer(json_layer, context)
    # Use QgsMapBoxGlStyleConverter.parseSymbolLayerAsRenderer
    has_renderer_style, renderer_style = (
        core_converter.parseSymbolLayerAsRenderer(json_layer, context))

    return (
        has_renderer_style,
        renderer_style,
        has_labeling_style,
        labeling_style,
    )


def parse_interpolate_color_by_zoom(json_fill_color, context):
    base = json_fill_color["base"] if "base" in json_fill_color else 1
    stops = json_fill_color["stops"]
    # Bottom color
    case_str = "CASE "

    if len(stops) == 0:
        return QgsProperty()

    for i in range(0, len(stops) - 1):
        bz = stops[i][0]
        tz = stops[i + 1][0]
        bottom_color = parse_color(stops[i][1], context)
        top_color = parse_color(stops[i + 1][1], context)

        bc_hue, bc_sat, bc_light, bc_alpha = get_color_as_hsla_components(
            bottom_color)
        tc_hue, tc_sat, tc_light, tc_alpha = get_color_as_hsla_components(
            top_color)

        case_str += (
            f"WHEN @vector_tile_zoom >= {bz} AND @vector_tile_zoom < {tz} THEN color_hsla("
            f"{interpolate_expression(bz, tz, bc_hue, tc_hue, base)}, "
            f"{interpolate_expression(bz, tz, bc_sat, tc_sat, base)}, "
            f"{interpolate_expression(bz, tz, bc_light, tc_light, base)}, "
            f"{interpolate_expression(bz, tz, bc_alpha, tc_alpha, base)}) ")

    # Top color
    tz = stops[-1][0]
    top_color = parse_color(stops[-1][1], context)
    tc_hue, tc_sat, tc_light, tc_alpha = get_color_as_hsla_components(
        top_color)
    case_str += (
        f"WHEN @vector_tile_zoom >= {tz} THEN color_hsla({tc_hue}, {tc_sat}, {tc_light}, {tc_alpha}) "
        f"ELSE color_hsla({tc_hue}, {tc_sat}, {tc_light}, {tc_alpha}) END")

    return QgsProperty.fromExpression(case_str)


def parse_interpolate_by_zoom(
    json_obj: dict,
    context: QgsMapBoxGlStyleConversionContext,
    multiplier: float,
):
    base = json_obj.get("base") if json_obj.get("base") else 1
    stops = json_obj.get("stops")

    scale_expression = parse_stops(base, stops, multiplier, context)

    if isinstance(scale_expression, (int, float)):
        return scale_expression
    else:
        return QgsProperty.fromExpression(scale_expression)


def parse_interpolate_opacity_by_zoom(json_obj: dict, max_opacity: int):
    base = json_obj.get("base") if json_obj.get("base") else 1
    stops = json_obj.get("stops")

    if len(stops) == 0:
        return QgsProperty()
    if len(stops) <= 2:
        scale_exp = (
            f"set_color_part(@symbol_color, 'alpha', "
            f"{interpolate_expression(stops[0][0], stops[-1][0],stops[0][1] * max_opacity, stops[-1][1] * max_opacity, base)})"
        )
    else:
        scale_exp = parse_opacity_stops(base, stops, max_opacity)

    return QgsProperty.fromExpression(scale_exp)


def parse_opacity_stops(base: (int, float), stops: list, max_opacity: int):
    case_str = (
        f"CASE WHEN @vector_tile_zoom < {stops[0][0]} THEN set_color_part(@symbol_color, 'alpha', "
        f"{stops[0][1]*max_opacity})")

    for i in range(len(stops) - 1):
        case_str += (
            f" WHEN @vector_tile_zoom >= {stops[i][0]} AND @vector_tile_zoom < {stops[i + 1][0]} "
            f"THEN set_color_part(@symbol_color, 'alpha', "
            f"{interpolate_expression(stops[i][0], stops[i+1][0], stops[i][1] * max_opacity, stops[i+1][1], max_opacity, base)})"
        )
    case_str += (
        f" WHEN @vector_tile_zoom >= {stops[-1][0]}"
        f" THEN set_color_part(@symbol_color, 'alpha', {stops[-1][1] * 255.0}) END"
    )
    return case_str


def parse_interpolate_point_by_zoom(
    json_obj: dict,
    context: QgsMapBoxGlStyleConversionContext,
    multiplier: float,
):
    base = json_obj.get("base") if json_obj.get("base") else 1
    stops = json_obj.get("stops")

    if len(stops) == 0:
        return QgsProperty()

    if len(stops) <= 2:
        scale_expr = (
            f"array("
            f"{interpolate_expression(stops[0][0], stops[-1][0], stops[0][1][0], stops[-1][1][0], base, multiplier)}, "
            f"{interpolate_expression(stops[0][0], stops[-1][0], stops[0][1][1], stops[-1][1][1], base, multiplier)})"
        )
        scale_expr = parse_point_stops(base, stops, context, multiplier)

    else:
        scale_expr = parse_point_stops(base, stops, context, multiplier)

    return QgsProperty.fromExpression(scale_expr)


def parse_interpolate_string_by_zoom(
        json_dict: dict, context: QgsMapBoxGlStyleConversionContext,
        conversion_map):
    stops = json_dict.get("stops")
    if not stops:
        return QgsProperty()
    scale_expression = parse_string_stops(stops, context, conversion_map)
    return QgsProperty.fromExpression(scale_expression)


def parse_point_stops(
        base: float,
        stops: list,
        context: QgsMapBoxGlStyleConversionContext,
        multiplier: (int, float),
):
    case_str = "CASE "
    for i in range(len(stops) - 1):
        bz = stops[i][0]
        bv = stops[i][-1]
        if not isinstance(bv, list):
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported offset interpolation type ({type(bz).__name__})."
            )
            return

        tz = stops[i + 1][0]
        tv = stops[i + 1][-1]
        if not isinstance(tv, list):
            context.pushWarning(
                f"{context.layerId()}: Skipping unsupported offset interpolation type ({type(bz).__name__})."
            )
            return
    case_str += (
        f"WHEN @vector_tile_zoom > {bz} AND @vector_tile_zoom <= {tz} "
        f"THEN array("
        f"{interpolate_expression(bz, tz, bv[0], tv[0], base, multiplier)}, "
        f"{interpolate_expression(bz, tz, bv[1], tv[1], base, multiplier)}) ")
    case_str += "END"
    return case_str


def parse_stops(
        base: (int, float),
        stops: list,
        multiplier: (int, float),
        context: QgsMapBoxGlStyleConversionContext,
):
    case_str = "CASE "
    # First zoom/value
    fz = stops[0][0]
    fv = stops[0][1]
    if isinstance(fv, list):
        fv = parse_expression(fv, context)
    case_str += f"WHEN @vector_tile_zoom <= {fz} " f"THEN {fv} * {multiplier} "
    for i in range(len(stops) - 1):
        bz = stops[i][0]
        bv = stops[i][1]
        if isinstance(bv, list):
            bv = parse_expression(bv, context)
        tz = stops[i + 1][0]
        tv = stops[i + 1][1]
        if isinstance(tv, list):
            tv = parse_expression(tv, context)

        case_str += (
            f"WHEN @vector_tile_zoom > {bz} AND @vector_tile_zoom <= {tz} "
            f"THEN {interpolate_expression(bz, tz, bv, tv, base, multiplier)} "
        )

    z = stops[-1][0]
    v = stops[-1][1]
    if isinstance(v, list):
        v = parse_expression(v, context)
    case_str += f"WHEN @vector_tile_zoom > {z} THEN {v} * {multiplier} END"
    return case_str


def parse_discrete(json_list: list,
                   context: QgsMapBoxGlStyleConversionContext):
    attr = parse_expression(json_list[1], context)

    if not attr:
        context.pushWarning(
            f"{context.layerId()}: Could not interpret step expression.")
        return

    case_str = "CASE "
    for i in range(2, len(json_list) - 1, 2):
        # WHEN value
        when_value = json_list[i]
        # THEN value
        then_value = json_list[i + 1]
        # IN operator for list
        if isinstance(when_value, list):
            match_str_lst = [
                QgsExpression.quotedValue(wv) for wv in when_value
            ]
            case_str += f"WHEN {attr} IN ({','.join(match_str_lst)}) THEN {then_value} "
        # EQUAL operator for single key
        elif isinstance(when_value, str):
            case_str += f"WHEN {attr}={QgsExpression.quotedValue(when_value)} THEN {then_value} "

    else_value = json_list[-1]
    case_str += f"ELSE {else_value} END"
    return case_str


def parse_concat(json_list: list, context: QgsMapBoxGlStyleConversionContext):
    concat_items = list(map(parse_expression, json_list[1:], repeat(context)))
    concat_str = f"concat({','.join(concat_items)})"
    return concat_str


def parse_case(json_list: list, context: QgsMapBoxGlStyleConversionContext):
    case_str = "CASE "
    for i in range(1, len(json_list) - 1, 2):
        # WHEN value
        when_value = json_list[i]
        # THEN value
        then_value = json_list[i + 1]
        if isinstance(when_value, list):
            when_expr = parse_expression(when_value, context)
            case_str += f"WHEN {when_expr} THEN {then_value} "
        # EQUAL operator for single key
        elif isinstance(when_value, str):
            case_str += f"WHEN {QgsExpression.quotedValue(when_value)} THEN {then_value} "
    else_value = json_list[-1]
    case_str += f"ELSE {else_value} END"
    return case_str


def parse_coalesce(json_list: list,
                   context: QgsMapBoxGlStyleConversionContext):
    coalesce_items = list(map(parse_expression, json_list[1:],
                              repeat(context)))
    coalesce_str = f"COALESCE({','.join(coalesce_items)})"
    return coalesce_str


def parse_array_stops(stops: list, multiplier: (int, float)):
    if len(stops) < 2:
        return
    case_str = "CASE "
    for i in range(len(stops) - 1):
        bz = stops[i][0]
        bv = stops[i][1]
        bl = []
        for v in bv:
            bl.append(str(float(v) * multiplier))
        tz = stops[i + 1][0]

        case_str += f"WHEN @vector_tile_zoom > {bz} AND @vector_tile_zoom <= {tz} THEN array({','.join(bl)}) "

    lz = stops[-1][0]
    lv = stops[-1][1]
    ll = []
    for v in lv:
        ll.append(str(float(v) * multiplier))
    case_str += f"WHEN @vector_tile_zoom > {lz} THEN array({','.join(ll)}) "
    case_str += "END"
    return case_str


def process_label_field(string: str):
    # Convert field name
    # {field_name} is permitted in string -- if multiple fields are present, convert them to an expression
    # but if single field is covered in {}, return it directly
    string = string.strip()
    single_field_rx = QRegularExpression("^{([^}]+)}$")
    match = single_field_rx.match(string)
    if match.hasMatch():
        is_expression = True
        return QgsExpression.quotedColumnRef(match.captured(1)), is_expression
    multi_field_rx = "(?={[^}]+})"
    parts = re.split(multi_field_rx, string)
    if len(parts) > 1:
        is_expression = True
        res = []
        for part in parts:
            if not part:
                continue
            elif "}" in part:
                # part will start at a {field} reference
                split = part.split("}")
                res.append(QgsExpression.quotedColumnRef(split[0][1:]))
                if split[1]:
                    res.append(QgsExpression.quotedValue(split[1]))
            else:
                res.append(QgsExpression.quotedValue(part))
        return f"concat({','.join(res)})", is_expression
    else:
        is_expression = False
    return QgsExpression.quotedColumnRef(string), is_expression


def parse_string_stops(stops, context, conversion_map):
    case_str = "CASE "
    for i in range(len(stops) - 1):
        # Bottom zoom and value
        bz = stops[i][0]
        bv = stops[i][1]

        if isinstance(bz, list):
            context.pushWarning(
                f"{context.layerId()}: Expressions in interplation function are not supported, skipping."
            )
            return

        # Top zoom
        tz = stops[i + 1][0]
        if isinstance(tz, list):
            context.pushWarning(
                f"{context.layerId()}: Expressions in interplation function are not supported, skipping."
            )
            return

        case_str += (
            f"WHEN @vector_tile_zoom > {bz} AND @vector_tile_zoom <= {tz} "
            f"THEN {QgsExpression.quotedValue(conversion_map[bv][bv])} ")
    case_str += f"ELSE {QgsExpression.quotedValue(conversion_map[stops[-1][1]][stops[-1][1]])} END"

    return case_str


def parse_value_list(
    json_list: list,
    property_type: PropertyType,
    context: QgsMapBoxGlStyleConversionContext,
    multiplier: float,
    max_opacity: int,
):
    method = json_list[0]
    if method == "interpolate":
        return parse_interpolate_list_by_zoom(json_list, property_type,
                                              context, multiplier, max_opacity)
    elif method == "match":
        return parse_match_list(json_list, property_type, context, multiplier,
                                max_opacity)
    else:
        context.pushWarning(
            f"{context.layerId()}: Could not interpret value list with method {method}"
        )
        return QgsProperty()


def parse_match_list(
    json_list: list,
    property_type: PropertyType,
    context: QgsMapBoxGlStyleConversionContext,
    multiplier: float,
    max_opacity: int,
):
    attr = parse_expression(json_list[1], context)

    if not attr:
        context.pushWarning(
            f"{context.layerId()}: Could not interpret match list.")
        return QgsProperty()

    case_str = "CASE "
    for i in range(2, len(json_list) - 1, 2):
        # THEN value
        then_value = json_list[i + 1]
        if property_type == PropertyType.Color:
            color = parse_color(then_value, context)
            then_value_str = QgsExpression.quotedString(
                color.name(QColor.NameFormat.HexArgb))
        elif property_type == PropertyType.Numeric:
            then_value_str = str(then_value * multiplier)
        elif property_type == PropertyType.Opacity:
            then_value_str = str(then_value * max_opacity)
        elif property_type == PropertyType.Point:
            then_value_str = f"array({then_value[0] * multiplier},{then_value[0] * multiplier})"
        # WHEN value
        when_value = json_list[i]
        # IN operator for list
        if isinstance(when_value, list):
            match_str_lst = [
                QgsExpression.quotedValue(wv) for wv in when_value
            ]
            case_str += f"WHEN {attr} IN ({','.join(match_str_lst)}) THEN {then_value_str} "
        # EQUAL operator for single key
        elif isinstance(when_value, (str, int, float)):
            case_str += f"WHEN {attr}={QgsExpression.quotedValue(when_value)} THEN {then_value_str} "

    if property_type == PropertyType.Color:
        color = parse_color(json_list[-1], context)
        else_value = QgsExpression.quotedString(
            color.name(QColor.NameFormat.HexArgb))
    elif property_type == PropertyType.Numeric:
        v = json_list[-1] * multiplier
        else_value = str(v)
    elif property_type == PropertyType.Opacity:
        v = json_list[-1] * max_opacity
        else_value = str(v)
    elif property_type == PropertyType.Point:
        else_value = (f"array("
                      f"{json_list[-1][0] * multiplier},"
                      f"{json_list[-1][0] * multiplier})")
    case_str += f"ELSE {else_value} END"
    return QgsProperty.fromExpression(case_str)


def parse_interpolate_list_by_zoom(
    json_list: list,
    type: PropertyType,
    context: QgsMapBoxGlStyleConversionContext,
    multiplier: float,
    max_opacity: int,
):
    if json_list[0] != "interpolate":
        context.pushWarning(
            f"{context.layerId()}: Could not interpret value list.")
        return QgsProperty()

    technique = json_list[1][0]

    if technique == "linear":
        base = 1
    elif technique == "exponential":
        base = json_list[1][1]
    elif technique == "cubic-bezier":
        context.pushWarning(
            f"{context.layerId()}: Cubic-bezier interpolation is not supported, linear used instead."
        )
        base = 1
    else:
        context.pushWarning(
            f"{context.layerId()}: Skipping not implemented interpolation method {technique}"
        )
        return QgsProperty()

    if json_list[2][0] != "zoom":
        context.pushWarning(
            f"{context.layerId()}: Skipping not implemented interpolation input {json_list[2]}"
        )
        return QgsProperty()

    # Convert stops into list of lists
    list_stops_values = json_list[3:]
    it = iter(list_stops_values)
    stops = list(zip(it, it))
    props = {"base": base, "stops": stops}

    if type == PropertyType.Color:
        return parse_interpolate_color_by_zoom(props, context)
    elif type == PropertyType.Numeric:
        return parse_interpolate_by_zoom(props, context, multiplier)
    elif type == PropertyType.Opacity:
        return parse_interpolate_opacity_by_zoom(props, max_opacity)
    elif type == PropertyType.Point:
        return parse_interpolate_point_by_zoom(props, context, multiplier)

    return QgsProperty()


def parse_color(json_color: str, context: QgsMapBoxGlStyleConversionContext):
    if not isinstance(json_color, str):
        context.pushWarning(
            f"{context.layerId()}: Could not parse non-string color {json_color}, skipping."
        )
        return None
    return QgsSymbolLayerUtils.parseColor(json_color)


def get_color_as_hsla_components(qcolor: QColor):
    """
    Takes QColor object and returns HSLA components in required format for QGIS color_hsla() function.
    hue: an integer value from 0 to 360,
    saturation: an integer value from 0 to 100,
    lightness: an integer value from 0 to 100,
    alpha: an integer value from 0 (completely transparent) to 255 (opaque).
    """
    hue = int(max(0, qcolor.hslHue()))
    sat = int(qcolor.hslSaturation() / 255.0 * 100)
    lightness = int(qcolor.lightness() / 255 * 100)
    alpha = int(qcolor.alpha())
    return hue, sat, lightness, alpha


def interpolate_expression(
    zoom_min: float,
    zoom_max: float,
    value_min: float,
    value_max: float,
    base: float,
    multiplier: float = 1,
):
    if (isinstance(value_min,
                   (int, float)) and isinstance(value_max, (int, float))
            and qgsDoubleNear(value_min, value_max)):
        return value_min * multiplier

    if base == 1:
        expression = f"scale_linear(@vector_tile_zoom,{zoom_min},{zoom_max},{value_min},{value_max})"
    else:
        expression = (
            f"{value_min} + ({value_max} - {value_min}) * "
            f"({base}^(@vector_tile_zoom-{zoom_min})-1)/({base}^({zoom_max}-{zoom_min})-1)"
        )

    if multiplier != 1:
        return f"({expression}) * {multiplier}"
    else:
        return expression


def parse_cap_style(style: str):
    if style == "round":
        return Qt.PenCapStyle.RoundCap
    elif style == "square":
        return Qt.PenCapStyle.SquareCap
    else:
        # default
        return Qt.PenCapStyle.FlatCap


def parse_join_style(style: str):
    if style == "bevel":
        return Qt.PenJoinStyle.BevelJoin
    elif style == "round":
        return Qt.PenJoinStyle.RoundJoin
    else:
        # default
        return Qt.PenJoinStyle.MiterJoin


def parse_expression(json_expr, context):
    """Parses expression into QGIS expression string"""
    if isinstance(json_expr, str):
        return QgsExpression.quotedValue(json_expr)

    op = json_expr[0]

    if op in ("all", "any", "none"):
        lst = [parse_value(v, context) for v in json_expr[1:]]
        if None in lst:
            context.pushWarning(
                f"{context.layerId}: Skipping unsupported expression.")
            return
        if op == "none":
            operator_string = ") AND NOT ("
            return f"NOT ({operator_string.join(lst)})"
        if op == "all":
            operator_string = ") AND ("
        elif op == "any":
            operator_string = ") OR ("
        return f"({operator_string.join(lst)})"
    elif op == "!":
        # ! inverts next expression meaning
        contra_json_expr = json_expr[1]
        contra_json_expr[0] = op + contra_json_expr[0]
        # ['!', ['has', 'level']] -> ['!has', 'level']
        return parse_key(contra_json_expr, context)
    elif op in ("==", "!=", ">=", ">", "<=", "<"):
        # use IS and NOT IS instead of = and != because they can deal with NULL values
        if op == "==":
            op = "IS"
        elif op == "!=":
            op = "IS NOT"
        return f"{parse_key(json_expr[1], context)} {op} {parse_value(json_expr[2], context)}"
    elif op == "has":
        return parse_key(json_expr[1], context) + " IS NOT NULL"
    elif op == "!has":
        return parse_key(json_expr[1], context) + " IS NULL"
    elif op == "in" or op == "!in":
        key = parse_key(json_expr[1], context)
        lst = [parse_value(v, context) for v in json_expr[2:]]
        if None in lst:
            context.pushWarning(
                f"{context.layerId}: Skipping unsupported expression.")
            return
        if op == "in":
            return f"{key} IN ({', '.join(lst)})"
        else:  # not in
            return f"({key} IS NULL OR {key} NOT IN ({', '.join(lst)}))"
    elif op == "get":
        if json_expr[1].startswith("name"):
            return parse_key(json_expr[1], context)
        elif json_expr[1] == "class":
            return parse_key(json_expr[1], context)
        else:
            return f"attribute('{json_expr[1]}')"
    elif op == "match":
        attr = json_expr[1][1]

        if (len(json_expr) == 5 and isinstance(json_expr[3], bool)
                and isinstance(json_expr[4], bool)):
            if isinstance(json_expr[2], list):
                operator = "IN" if json_expr[3] is True else "NOT IN"
                lst = map(QgsExpression.quotedValue, json_expr[2])
                if len(json_expr[2]) > 1:
                    return f"{attr} IS NULL OR {QgsExpression.quotedColumnRef(attr)} {operator} ({', '.join(lst)})"
                else:
                    operator = "=" if json_expr[3] is True else "!="
                    return f"{QgsExpression.quotedColumnRef(attr)} {operator} '{json_expr[2][0]}'"
            elif isinstance(json_expr[2], (str, float, int)):
                return QgsExpression.createFieldEqualityExpression(
                    attr, json_expr[2])
            else:
                context.pushWarning(
                    f"{context.layerId}: Skipping unsupported expression.")
                return
        else:
            case_str = "CASE "
            for i in range(2, len(json_expr) - 2, 2):
                if isinstance(json_expr[i], (list, tuple)):
                    lst = list(map(QgsExpression.quotedValue, json_expr[i]))
                    if len(lst) > 1:
                        case_str += f"WHEN {QgsExpression.quotedValue(attr)} IN ({', '.join(lst)}) "
                    else:
                        case_str += f"WHEN {QgsExpression.createFieldEqualityExpression(attr, json_expr[i])} "
                elif isinstance(json_expr[i], (str, float, int)):
                    case_str += f"WHEN ({QgsExpression.createFieldEqualityExpression(attr, json_expr[i])}) "
                case_str += f"THEN {json_expr[i+1]} "
            case_str += f"ELSE {QgsExpression.quotedValue(json_expr[-1])} END"
            return case_str
    elif op == "to-string":
        return f"to_string({parse_expression(json_expr[1], context)})"
    elif op == "step":
        return parse_discrete(json_expr, context)
    elif op == "literal":
        field_name, field_is_expression = process_label_field(json_expr[1])
        return field_name
    elif op == "concat":
        return parse_concat(json_expr, context)
    elif op == "case":
        return parse_case(json_expr, context)
    elif op == "coalesce":
        return parse_coalesce(json_expr, context)
    elif op == "to-number":
        return f"to_real({parse_expression(json_expr[1], context)})"
    else:
        context.pushWarning(
            f"{context.layerId()}: Skipping unsupported expression.")
        return


def parse_value(json_value, context):
    if isinstance(json_value, list):
        return parse_expression(json_value, context)
    elif isinstance(json_value, str):
        return QgsExpression.quotedValue(json_value)
    elif isinstance(json_value, (int, float)):
        return str(json_value)
    else:
        context.pushWarning(
            f"{context.layerId}: Skipping unsupported expression part: {json_value}"
        )
        return None


def parse_key(json_key, context):
    if json_key == "$type" or json_key == "geometry-type":
        return "_geom_type"
    elif isinstance(json_key, list):
        if len(json_key) > 1:
            return parse_expression(json_key, context)
        else:
            return parse_key(json_key[0], context)
    elif isinstance(json_key, int):
        return json_key
    return QgsExpression.quotedColumnRef(json_key)


def parse_field_name_dict(json_obj, context):
    stops = json_obj.get("stops")

    if len(stops) == 0:
        return
    else:
        case_str = "CASE "
        for i in range(len(stops) - 1):
            bz = stops[i][0]
            bv = stops[i][1]
            if isinstance(bv, list):
                context.pushWarning(
                    f"{context.layerId()}: Expressions in field name are not supported, skipping."
                )
                return QgsExpression()
            processed_bv, is_expression = process_label_field(bv)
            tz = stops[i + 1][0]
            case_str += (
                f"WHEN @vector_tile_zoom > {bz} AND @vector_tile_zoom <= {tz} "
                f"THEN {processed_bv} ")

        z = stops[-1][0]
        v = stops[-1][1]
        processed_v, is_expression = process_label_field(v)
        case_str += f"WHEN @vector_tile_zoom > {z} THEN {processed_v} END"
        return case_str


def parse_svg_path(json_icon_image, map_id, context):
    ICONS_PATH = Path(
        os.path.dirname(os.path.realpath(__file__)).strip("gl2qgis"),
        "data",
        "icons",
    ).as_posix()
    if map_id == "openstreetmap":
        return QgsProperty.fromExpression(
            f"""'{ICONS_PATH}/{json_icon_image}.svg'""")
    if map_id == "bright":
        ICONS_PATH = f"{ICONS_PATH}/bright/"
    if isinstance(json_icon_image, str):
        image_parts = re.split("{|}", json_icon_image)
        concat_expr = f"concat('{ICONS_PATH}/', "
        for p in image_parts:
            if p:
                if not p.startswith("_") and not p.endswith("_"):
                    p = f"attribute('{p}')"
                    concat_expr = f"{concat_expr}{p}, "
                else:
                    concat_expr = f"{concat_expr}'{p}', "
        concat_expr = f"{concat_expr}'.svg')"
        return QgsProperty.fromExpression(concat_expr)
    elif isinstance(json_icon_image, list):
        if json_icon_image[0] == "concat":
            json_icon_image.insert(1, f"{ICONS_PATH}/")
            json_icon_image.append(".svg")
            concat_expr = parse_concat(json_icon_image, context)
            return QgsProperty.fromExpression(concat_expr)
    else:
        context.pushWarning(f"{context.layerId}: Cannot parse svg icon path.")
        return


def parse_background(bg_layer_data: dict):
    json_paint = bg_layer_data.get("paint")
    renderer = None
    core_converter = QgsMapBoxGlStyleConverter()
    if "background-color" in json_paint:
        sym = QgsSymbol.defaultSymbol(QgsWkbTypes.PolygonGeometry)
        # Set no stroke for background layer
        sym.symbolLayer(0).setStrokeColor(QColor("transparent"))
        # Parse fill color
        json_background_color = json_paint.get("background-color")
        context = QgsMapBoxGlStyleConversionContext()
        if isinstance(json_background_color, dict):
            bg_color_expr = core_converter.parseInterpolateColorByZoom(
                json_background_color, context)
            fill_symbol = sym.symbolLayer(0)
            fill_symbol.setDataDefinedProperty(
                QgsSymbolLayer.PropertyFillColor, bg_color_expr[0])
        elif isinstance(json_background_color, str):
            bg_color = core_converter.parseColor(json_background_color,
                                                 context)
            sym.symbolLayer(0).setColor(bg_color)
        elif isinstance(json_background_color, list):
            bg_color_expr = parse_value_list(json_background_color,
                                             PropertyType.Color, context, 1,
                                             255)
            fill_symbol = sym.symbolLayer(0)
            fill_symbol.setDataDefinedProperty(
                QgsSymbolLayer.PropertyFillColor, bg_color_expr)
        else:
            context.pushWarning(
                f"Background: Skipping not implemented expression for background color: "
                f"{json_background_color} , {type(json_background_color)}")
        if "background-opacity" in json_paint:
            json_background_opacity = json_paint.get("background-opacity")
            if isinstance(json_background_opacity, dict):
                sym.setOpacity(
                    core_converter.parseOpacity(json_background_opacity))
            elif isinstance(json_background_opacity, (int, float)):
                sym.setOpacity(json_background_opacity)
        renderer = QgsSingleSymbolRenderer(sym)
    return renderer


def get_sprites_from_style_json(style_json_data: dict):
    sprite_url = style_json_data.get("sprite")
    if sprite_url is None:
        return None, None

    # sprite_json_dict = json.loads(requests.get(sprite_url + '@2x.json').text)
    sprite_json_dict = utils.qgis_request_json(f"{sprite_url}@2x.json")
    sprite_img = QImage()
    sprite_img.loadFromData(utils.qgis_request_data(f"{sprite_url}@2x.png"))
    return sprite_json_dict, sprite_img
