"""
This is the base of the import tree for LVGL. It contains constant definitions used elsewhere.
Constants already defined in esphome.const are not duplicated here and must be imported where used.

"""

import logging
from typing import Any

from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType

from .helpers import requires_component

LOGGER = logging.getLogger(__name__)
lvgl_ns = cg.esphome_ns.namespace("lvgl")

DOMAIN = "lvgl"
KEY_LV_DEFINES = "lv_defines"
KEY_UPDATED_WIDGETS = "updated_widgets"


def get_data(key, default=None):
    """
    Get a data structure from the global data store by key
    :param key: A key for the data
    :param default: The default data - the default is an empty dict
    :return:
    """
    return CORE.data.setdefault(DOMAIN, {}).setdefault(
        key, default if default is not None else {}
    )


def add_define(macro, value="1"):
    lv_defines = get_data(KEY_LV_DEFINES)
    value = str(value)
    if lv_defines.setdefault(macro, value) != value:
        LOGGER.error(
            "Redefinition of %s - was %s now %s", macro, lv_defines[macro], value
        )
    lv_defines[macro] = value


def literal(arg) -> MockObj:
    if isinstance(arg, str):
        return MockObj(arg)
    return arg


def static_cast(type, value):
    return literal(f"static_cast<{type}>({value})")


def call_lambda(lamb: LambdaExpression):
    expr = lamb.content.strip()
    if expr.startswith("return") and expr.endswith(";"):
        return expr[6:-1].strip()
    # If lambda has parameters, call it with those parameter names
    # Parameter names come from hardcoded component code (like "x", "it", "event")
    # not from user input, so they're safe to use directly
    if lamb.parameters and lamb.parameters.parameters:
        param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters)
        return f"{lamb}({param_names})"
    return f"{lamb}()"


class LValidator:
    """
    A validator for a particular type used in LVGL. Usable in configs as a validator, also
    has `process()` to convert a value during code generation
    """

    def __init__(self, validator, rtype, retmapper=None, requires=None):
        self.validator = validator
        self.rtype = rtype
        self.retmapper = retmapper
        self.requires = requires

    def __call__(self, value):
        if self.requires:
            value = requires_component(self.requires)(value)
        if isinstance(value, cv.Lambda):
            return cv.returning_lambda(value)
        return self.validator(value)

    async def process(
        self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
    ) -> Expression:
        if value is None:
            return None
        if isinstance(value, Lambda):
            # Local import to avoid circular import
            from .lvcode import get_lambda_context_args

            args = args or get_lambda_context_args()
            return cg.RawExpression(
                call_lambda(
                    await cg.process_lambda(value, args, return_type=self.rtype)
                )
            )
        if self.retmapper is not None:
            return self.retmapper(value)
        if isinstance(value, ID):
            return await cg.get_variable(value)
        if isinstance(value, list):
            value = [
                await cg.get_variable(x) if isinstance(x, ID) else x for x in value
            ]
        return cg.safe_exp(value)


class LvConstant(LValidator):
    """
    Allow one of a list of choices, mapped to upper case, and prepend the choice with the prefix.
    It's also permitted to include the prefix in the value
    The property `one_of` has the single case validator, and `several_of` allows a list of constants.
    """

    def __init__(self, prefix: str, *choices):
        self.prefix = prefix
        self.choices = choices
        prefixed_choices = [prefix + v for v in choices]
        prefixed_validator = cv.one_of(*prefixed_choices, upper=True)

        @schema_extractor("one_of")
        def validator(value):
            if value == SCHEMA_EXTRACT:
                return self.choices
            if isinstance(value, str) and value.startswith(self.prefix):
                return prefixed_validator(value)
            return self.prefix + cv.one_of(*choices, upper=True)(value)

        super().__init__(validator, rtype=uint32)
        self.retmapper = self.mapper
        self.one_of = LValidator(validator, uint32, retmapper=self.mapper)
        self.several_of = LValidator(
            cv.ensure_list(self.one_of), uint32, retmapper=self.mapper
        )

    def mapper(self, value):
        if not isinstance(value, list):
            value = [value]
        return literal(
            "|".join(
                [
                    str(v) if str(v).startswith(self.prefix) else self.prefix + str(v)
                    for v in value
                ]
            ).upper()
        )

    def extend(self, *choices):
        """
        Extend an LVconstant with additional choices.
        :param choices: The extra choices
        :return: A new LVConstant instance
        """
        return LvConstant(self.prefix, *(self.choices + choices))


# Parts
CONF_MAIN = "main"
CONF_SCROLLBAR = "scrollbar"
CONF_INDICATOR = "indicator"
CONF_KNOB = "knob"
CONF_SELECTED = "selected"
CONF_TICKS = "ticks"
CONF_CURSOR = "cursor"
CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder"

# Layout types

TYPE_FLEX = "flex"
TYPE_GRID = "grid"
TYPE_NONE = "none"

DIRECTIONS = LvConstant("LV_DIR_", "LEFT", "RIGHT", "BOTTOM", "TOP")

LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [
    "dejavu_16_persian_hebrew",
    "simsun_16_cjk",
    "unscii_8",
    "unscii_16",
]

LV_EVENT_MAP = {
    "PRESS": "PRESSED",
    "SHORT_CLICK": "SHORT_CLICKED",
    "LONG_PRESS": "LONG_PRESSED",
    "LONG_PRESS_REPEAT": "LONG_PRESSED_REPEAT",
    "CLICK": "CLICKED",
    "RELEASE": "RELEASED",
    "SCROLL_BEGIN": "SCROLL_BEGIN",
    "SCROLL_END": "SCROLL_END",
    "SCROLL": "SCROLL",
    "FOCUS": "FOCUSED",
    "DEFOCUS": "DEFOCUSED",
    "READY": "READY",
    "CANCEL": "CANCEL",
    "ALL_EVENTS": "ALL",
    "CHANGE": "VALUE_CHANGED",
    "GESTURE": "GESTURE",
}

LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP)
SWIPE_TRIGGERS = tuple(
    f"on_swipe_{x.lower()}" for x in DIRECTIONS.choices + ("up", "down")
)


LV_ANIM = LvConstant(
    "LV_SCR_LOAD_ANIM_",
    "NONE",
    "OVER_LEFT",
    "OVER_RIGHT",
    "OVER_TOP",
    "OVER_BOTTOM",
    "MOVE_LEFT",
    "MOVE_RIGHT",
    "MOVE_TOP",
    "MOVE_BOTTOM",
    "FADE_IN",
    "FADE_OUT",
    "OUT_LEFT",
    "OUT_RIGHT",
    "OUT_TOP",
    "OUT_BOTTOM",
)

LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER")
LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF")

LV_LOG_LEVELS = {
    "VERBOSE": "TRACE",
    "DEBUG": "TRACE",
    "INFO": "INFO",
    "WARN": "WARN",
    "ERROR": "ERROR",
    "NONE": "NONE",
}

LV_LONG_MODES = LvConstant(
    "LV_LABEL_LONG_",
    "WRAP",
    "DOT",
    "SCROLL",
    "SCROLL_CIRCULAR",
    "CLIP",
)

STATES = (
    # default state not included here
    "checked",
    "focused",
    "focus_key",
    "edited",
    "hovered",
    "pressed",
    "scrolled",
    "disabled",
    "user_1",
    "user_2",
    "user_3",
    "user_4",
)

PARTS = (
    CONF_MAIN,
    CONF_SCROLLBAR,
    CONF_INDICATOR,
    CONF_KNOB,
    CONF_SELECTED,
    CONF_ITEMS,
    CONF_TICKS,
    CONF_CURSOR,
    CONF_TEXTAREA_PLACEHOLDER,
)

KEYBOARD_MODES = LvConstant(
    "LV_KEYBOARD_MODE_",
    "TEXT_LOWER",
    "TEXT_UPPER",
    "SPECIAL",
    "NUMBER",
)
ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE")
TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL")
SCROLL_DIRECTIONS = TILE_DIRECTIONS.extend("NONE")
SNAP_DIRECTIONS = LvConstant("LV_SCROLL_SNAP_", "NONE", "START", "END", "CENTER")
CHILD_ALIGNMENTS = LvConstant(
    "LV_ALIGN_",
    "TOP_LEFT",
    "TOP_MID",
    "TOP_RIGHT",
    "LEFT_MID",
    "CENTER",
    "RIGHT_MID",
    "BOTTOM_LEFT",
    "BOTTOM_MID",
    "BOTTOM_RIGHT",
)

SIBLING_ALIGNMENTS = LvConstant(
    "LV_ALIGN_",
    "OUT_LEFT_TOP",
    "OUT_TOP_LEFT",
    "OUT_TOP_MID",
    "OUT_TOP_RIGHT",
    "OUT_RIGHT_TOP",
    "OUT_LEFT_MID",
    "OUT_RIGHT_MID",
    "OUT_LEFT_BOTTOM",
    "OUT_BOTTOM_LEFT",
    "OUT_BOTTOM_MID",
    "OUT_BOTTOM_RIGHT",
    "OUT_RIGHT_BOTTOM",
)
ALIGN_ALIGNMENTS = CHILD_ALIGNMENTS.extend(*SIBLING_ALIGNMENTS.choices)

FLEX_FLOWS = LvConstant(
    "LV_FLEX_FLOW_",
    "ROW",
    "COLUMN",
    "ROW_WRAP",
    "COLUMN_WRAP",
    "ROW_REVERSE",
    "COLUMN_REVERSE",
    "ROW_WRAP_REVERSE",
    "COLUMN_WRAP_REVERSE",
)

OBJ_FLAGS = (
    "hidden",
    "clickable",
    "click_focusable",
    "checkable",
    "scrollable",
    "scroll_elastic",
    "scroll_momentum",
    "scroll_one",
    "scroll_chain_hor",
    "scroll_chain_ver",
    "scroll_chain",
    "scroll_on_focus",
    "scroll_with_arrow",
    "snappable",
    "press_lock",
    "event_bubble",
    "gesture_bubble",
    "adv_hittest",
    "ignore_layout",
    "floating",
    "overflow_visible",
    "layout_1",
    "layout_2",
    "widget_1",
    "widget_2",
    "user_1",
    "user_2",
    "user_3",
    "user_4",
)

ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")

BUTTONMATRIX_CTRLS = LvConstant(
    "LV_BTNMATRIX_CTRL_",
    "HIDDEN",
    "NO_REPEAT",
    "DISABLED",
    "CHECKABLE",
    "CHECKED",
    "CLICK_TRIG",
    "POPOVER",
    "RECOLOR",
    "CUSTOM_1",
    "CUSTOM_2",
)

LV_BASE_ALIGNMENTS = (
    "START",
    "CENTER",
    "END",
)
LV_CELL_ALIGNMENTS = LvConstant(
    "LV_GRID_ALIGN_",
    *LV_BASE_ALIGNMENTS,
)
LV_GRID_ALIGNMENTS = LV_CELL_ALIGNMENTS.extend(
    "STRETCH",
    "SPACE_EVENLY",
    "SPACE_AROUND",
    "SPACE_BETWEEN",
)

LV_FLEX_ALIGNMENTS = LvConstant(
    "LV_FLEX_ALIGN_",
    *LV_BASE_ALIGNMENTS,
    "SPACE_EVENLY",
    "SPACE_AROUND",
    "SPACE_BETWEEN",
)

LV_FLEX_CROSS_ALIGNMENTS = LV_FLEX_ALIGNMENTS.extend("STRETCH")

LV_MENU_MODES = LvConstant(
    "LV_MENU_HEADER_",
    "TOP_FIXED",
    "TOP_UNFIXED",
    "BOTTOM_FIXED",
)

LV_CHART_TYPES = (
    "NONE",
    "LINE",
    "BAR",
    "SCATTER",
)
LV_CHART_AXES = (
    "PRIMARY_Y",
    "SECONDARY_Y",
    "PRIMARY_X",
    "SECONDARY_X",
)

CONF_ACCEPTED_CHARS = "accepted_chars"
CONF_ADJUSTABLE = "adjustable"
CONF_ALIGN = "align"
CONF_ALIGN_TO = "align_to"
CONF_ANIMATED = "animated"
CONF_ANIMATION = "animation"
CONF_ANTIALIAS = "antialias"
CONF_ARC_LENGTH = "arc_length"
CONF_AUTO_START = "auto_start"
CONF_BACKGROUND_STYLE = "background_style"
CONF_BUTTON_STYLE = "button_style"
CONF_DECIMAL_PLACES = "decimal_places"
CONF_COLUMN = "column"
CONF_DIGITS = "digits"
CONF_DISP_BG_COLOR = "disp_bg_color"
CONF_DISP_BG_IMAGE = "disp_bg_image"
CONF_DISP_BG_OPA = "disp_bg_opa"
CONF_BODY = "body"
CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order"
CONF_CHANGE_RATE = "change_rate"
CONF_CLOSE_BUTTON = "close_button"
CONF_CONTAINER = "container"
CONF_CONTROL = "control"
CONF_DEFAULT_FONT = "default_font"
CONF_DEFAULT_GROUP = "default_group"
CONF_DIR = "dir"
CONF_DISPLAYS = "displays"
CONF_EDITING = "editing"
CONF_ENCODERS = "encoders"
CONF_END_ANGLE = "end_angle"
CONF_END_VALUE = "end_value"
CONF_ENTER_BUTTON = "enter_button"
CONF_ENTRIES = "entries"
CONF_FLAGS = "flags"
CONF_FLEX_FLOW = "flex_flow"
CONF_FLEX_ALIGN_MAIN = "flex_align_main"
CONF_FLEX_ALIGN_CROSS = "flex_align_cross"
CONF_FLEX_ALIGN_TRACK = "flex_align_track"
CONF_FLEX_GROW = "flex_grow"
CONF_FREEZE = "freeze"
CONF_FULL_REFRESH = "full_refresh"
CONF_GRADIENTS = "gradients"
CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos"
CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos"
CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span"
CONF_GRID_CELL_COLUMN_SPAN = "grid_cell_column_span"
CONF_GRID_CELL_X_ALIGN = "grid_cell_x_align"
CONF_GRID_CELL_Y_ALIGN = "grid_cell_y_align"
CONF_GRID_COLUMN_ALIGN = "grid_column_align"
CONF_GRID_COLUMNS = "grid_columns"
CONF_GRID_ROW_ALIGN = "grid_row_align"
CONF_GRID_ROWS = "grid_rows"
CONF_HEADER_MODE = "header_mode"
CONF_HOME = "home"
CONF_INITIAL_FOCUS = "initial_focus"
CONF_SELECTED_DIGIT = "selected_digit"
CONF_KEY_CODE = "key_code"
CONF_KEYPADS = "keypads"
CONF_LAYOUT = "layout"
CONF_LEFT_BUTTON = "left_button"
CONF_LINE_WIDTH = "line_width"
CONF_LONG_PRESS_TIME = "long_press_time"
CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time"
CONF_LVGL_ID = "lvgl_id"
CONF_LONG_MODE = "long_mode"
CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj"
CONF_ONE_CHECKED = "one_checked"
CONF_ONE_LINE = "one_line"
CONF_ON_DRAW_START = "on_draw_start"
CONF_ON_DRAW_END = "on_draw_end"
CONF_ON_PAUSE = "on_pause"
CONF_ON_RESUME = "on_resume"
CONF_ON_SELECT = "on_select"
CONF_OPA = "opa"
CONF_NEXT = "next"
CONF_PAD_ROW = "pad_row"
CONF_PAD_COLUMN = "pad_column"
CONF_PAGE = "page"
CONF_PAGE_WRAP = "page_wrap"
CONF_PASSWORD_MODE = "password_mode"
CONF_PIVOT_X = "pivot_x"
CONF_PIVOT_Y = "pivot_y"
CONF_PLACEHOLDER_TEXT = "placeholder_text"
CONF_POINTS = "points"
CONF_PREVIOUS = "previous"
CONF_REPEAT_COUNT = "repeat_count"
CONF_RECOLOR = "recolor"
CONF_RESUME_ON_INPUT = "resume_on_input"
CONF_RIGHT_BUTTON = "right_button"
CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn"
CONF_SCALE_LINES = "scale_lines"
CONF_SCROLLBAR_MODE = "scrollbar_mode"
CONF_SCROLL_DIR = "scroll_dir"
CONF_SCROLL_SNAP_X = "scroll_snap_x"
CONF_SCROLL_SNAP_Y = "scroll_snap_y"
CONF_SELECTED_INDEX = "selected_index"
CONF_SELECTED_TEXT = "selected_text"
CONF_SHOW_SNOW = "show_snow"
CONF_SPIN_TIME = "spin_time"
CONF_SRC = "src"
CONF_START_ANGLE = "start_angle"
CONF_START_VALUE = "start_value"
CONF_STATES = "states"
CONF_STYLE = "style"
CONF_STYLES = "styles"
CONF_STYLE_DEFINITIONS = "style_definitions"
CONF_STYLE_ID = "style_id"
CONF_SKIP = "skip"
CONF_SYMBOL = "symbol"
CONF_TAB_ID = "tab_id"
CONF_TABS = "tabs"
CONF_TIME_FORMAT = "time_format"
CONF_TILE = "tile"
CONF_TILE_ID = "tile_id"
CONF_TILES = "tiles"
CONF_TITLE = "title"
CONF_TOP_LAYER = "top_layer"
CONF_TOUCHSCREENS = "touchscreens"
CONF_TRANSPARENCY_KEY = "transparency_key"
CONF_THEME = "theme"
CONF_UPDATE_ON_RELEASE = "update_on_release"
CONF_UPDATE_WHEN_DISPLAY_IDLE = "update_when_display_idle"
CONF_VISIBLE_ROW_COUNT = "visible_row_count"
CONF_WIDGET = "widget"
CONF_WIDGETS = "widgets"
CONF_ZOOM = "zoom"

# Keypad keys

LV_KEYS = LvConstant(
    "LV_KEY_",
    "UP",
    "DOWN",
    "RIGHT",
    "LEFT",
    "ESC",
    "DEL",
    "BACKSPACE",
    "ENTER",
    "NEXT",
    "PREV",
    "HOME",
    "END",
)


DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"


def join_enums(enums, prefix=""):
    enums = list(enums)
    enums.sort()
    # If a prefix is provided, prepend each constant with the prefix, and assume that all the constants are within the
    # same namespace, otherwise cast to int to avoid triggering warnings about mixing enum types.
    if prefix:
        return literal("|".join(f"{prefix}{e.upper()}" for e in enums))
    return literal("|".join(f"(int){e.upper()}" for e in enums))
