@tool
@icon("res://addons/kenyoni/qr_code/icon.svg")
extends TextureRect
class_name QRCodeRect

const QRCode := preload("res://addons/kenyoni/qr_code/qr_code.gd")
const ShiftJIS := preload("res://addons/kenyoni/qr_code/shift_jis.gd")

var mode: QRCode.Mode:
    set = set_mode,
    get = get_mode
var error_correction: QRCode.ErrorCorrection:
    set = set_error_correction,
    get = get_error_correction
## Use Extended Channel Interpretation (ECI).
var use_eci: bool:
    set = set_use_eci,
    get = get_use_eci
## Extended Channel Interpretation (ECI) Value.
var eci_value: int:
    set = set_eci_value,
    get = get_eci_value
var data: Variant = "":
    set = set_data,
    get = get_data
## Automatically update the QR Code when a property changes.
var auto_update: bool = true:
    set = set_auto_update
## Use automatically the smallest version possible.
var auto_version: bool = true:
    set = set_auto_version,
    get = get_auto_version
var version: int = 1:
    set = set_version,
    get = get_version
## Use automatically the best mask pattern.
var auto_mask_pattern: bool = true:
    set = set_auto_mask_pattern,
    get = get_auto_mask_pattern
## Used mask pattern.
var mask_pattern: int = 0:
    set = set_mask_pattern,
    get = get_mask_pattern
var light_module_color: Color = Color.WHITE:
    set = set_light_module_color
var dark_module_color: Color = Color.BLACK:
    set = set_dark_module_color
## Automatically set the module pixel size based on the size.
## Do not use expand mode KEEP_SIZE when using it.
## Turn this off when the QR Code is resized often, as it impacts the performance quite heavily.
var auto_module_size: bool = true:
    set = set_auto_module_size
## Use that many pixel for one module.
## If auto_module_size is set, this value might by only occasionally updated. In this case do not rely on it.
var module_size: int = 1:
    set = set_module_size
## Use that many modules for the quiet zone. A value of 4 is recommended.
var quiet_zone_size: int = 4:
    set = set_quiet_zone_size

var _qr: QRCode = QRCode.new()
var _update_fn: Callable = self.update
var _cached_data: PackedByteArray = PackedByteArray()

func set_mode(new_mode: QRCode.Mode) -> void:
    self._qr.mode = new_mode
    self.notify_property_list_changed()
    if self.auto_update:
        self.update()

func get_mode() -> QRCode.Mode:
    return self._qr.mode

func set_error_correction(new_error_correction: QRCode.ErrorCorrection) -> void:
    self._qr.error_correction = new_error_correction
    if self.auto_update:
        self.update()

func get_error_correction() -> QRCode.ErrorCorrection:
    return self._qr.error_correction

func set_use_eci(new_use_eci: bool) -> void:
    self._qr.use_eci = new_use_eci
    self.notify_property_list_changed()
    if self.auto_update:
        self.update()

func get_use_eci() -> bool:
    return self._qr.use_eci

func set_eci_value(new_eci_value: int) -> void:
    if self._qr.eci_value == new_eci_value:
        return
    self._qr.eci_value = new_eci_value
    self.notify_property_list_changed()
    if self.auto_update:
        self.update()

func get_eci_value() -> int:
    return self._qr.eci_value

func set_data(new_data: Variant) -> void:
    match self._qr.mode:
        QRCode.Mode.NUMERIC:
            self._qr.put_numeric(new_data)
        QRCode.Mode.ALPHANUMERIC:
            self._qr.put_alphanumeric(new_data)
        QRCode.Mode.BYTE:
            if typeof(new_data) == TYPE_PACKED_BYTE_ARRAY || ! self.use_eci:
                self._qr.put_byte(new_data)
                return
            match self.eci_value:
                QRCode.ECI.ISO_8859_1:
                    self._qr.put_byte(new_data.to_ascii_buffer())
                QRCode.ECI.SHIFT_JIS:
                    self._qr.put_byte(ShiftJIS.to_shift_jis_2004_buffer(new_data))
                QRCode.ECI.UTF_8:
                    self._qr.put_byte(new_data.to_utf8_buffer())
                QRCode.ECI.UTF_16:
                    self._qr.put_byte(new_data.to_utf16_buffer())
                QRCode.ECI.US_ASCII:
                    self._qr.put_byte(new_data.to_ascii_buffer())
                _:
                    self._qr.put_byte(new_data)
        QRCode.Mode.KANJI:
            self._qr.put_kanji(new_data)

    if self.auto_update:
        self.update()

func get_data() -> Variant:
    var input_data: Variant = self._qr.get_input_data()
    if self.mode == QRCode.Mode.BYTE && self.use_eci:
        match self.eci_value:
            QRCode.ECI.ISO_8859_1:
                return input_data.get_string_from_ascii()
            QRCode.ECI.SHIFT_JIS:
                return ShiftJIS.get_string_from_shift_jis_2004(input_data)
            QRCode.ECI.UTF_8:
                return input_data.get_string_from_utf8()
            QRCode.ECI.UTF_16:
                return input_data.get_string_from_utf16()
            QRCode.ECI.US_ASCII:
                return input_data.get_string_from_ascii()

    return self._qr.get_input_data()

func set_auto_update(new_auto_update: bool) -> void:
    if auto_update == new_auto_update:
        return
    auto_update = new_auto_update
    self.notify_property_list_changed()
    if auto_update:
        self.update()

func set_auto_version(new_auto_version: bool) -> void:
    if self._qr.auto_version == new_auto_version:
        return
    self._qr.auto_version = new_auto_version
    self.notify_property_list_changed()
    if self.auto_update:
        self.update()

func get_auto_version() -> bool:
    return self._qr.auto_version

func set_version(new_version: int) -> void:
    if self.auto_version || self._qr.version == new_version:
        return
    self._qr.version = new_version
    if self.auto_update:
        self.update()

func get_version() -> int:
    return self._qr.version

func set_auto_mask_pattern(new_auto_mask_pattern: bool) -> void:
    if self._qr.auto_mask_pattern == new_auto_mask_pattern:
        return
    self._qr.auto_mask_pattern = new_auto_mask_pattern
    self.notify_property_list_changed()
    if self.auto_update:
        self.update()

func get_auto_mask_pattern() -> bool:
    return self._qr.auto_mask_pattern

func set_mask_pattern(new_mask_pattern: int) -> void:
    if self.auto_mask_pattern || self._qr.mask_pattern == new_mask_pattern:
        return
    self._qr.mask_pattern = new_mask_pattern
    if self.auto_update:
        self.update()

func get_mask_pattern() -> int:
    return self._qr.mask_pattern

func set_light_module_color(new_light_module_color: Color) -> void:
    if light_module_color == new_light_module_color:
        return
    light_module_color = new_light_module_color
    if self.auto_update:
        self._update_texture()

func set_dark_module_color(new_dark_module_color: Color) -> void:
    if dark_module_color == new_dark_module_color:
        return
    dark_module_color = new_dark_module_color
    if self.auto_update:
        self._update_texture()

func set_auto_module_size(new_auto_module_size: bool) -> void:
    if auto_module_size == new_auto_module_size:
        return
    auto_module_size = new_auto_module_size
    self.notify_property_list_changed()
    self.update_configuration_warnings()
    if self.auto_update:
        self._update_texture()

func set_module_size(new_module_size: int) -> void:
    if module_size == new_module_size:
        return
    module_size = new_module_size
    # if not auto it was set directly
    if ! self.auto_module_size && self.auto_update:
        self._update_texture()

func set_quiet_zone_size(new_quiet_zone_size: int) -> void:
    if quiet_zone_size == new_quiet_zone_size:
        return
    quiet_zone_size = maxi(0, new_quiet_zone_size)
    if self.auto_module_size && self.auto_update:
        self._update_texture()
    elif self.auto_module_size:
        self._update_module_size()

func _ready() -> void:
    if self.texture != null && self.auto_update:
        self.update()

func update() -> void:
    self._cached_data = self._qr.encode()
    self._update_texture()

func _set(property: StringName, value: Variant) -> bool:
    if property == "expand_mode":
        self.update_configuration_warnings()
        
    return false

func _get_property_list() -> Array[Dictionary]:
    var data_prop: Dictionary = {
        "name": "data",
        "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
    }
    match self.mode:
        QRCode.Mode.NUMERIC:
            data_prop["type"] = TYPE_STRING
        QRCode.Mode.ALPHANUMERIC, QRCode.Mode.KANJI:
            data_prop["type"] = TYPE_STRING
            data_prop["hint"] = PROPERTY_HINT_MULTILINE_TEXT
        QRCode.Mode.BYTE:
            # these encoding is natively supported
            if self.use_eci && self.eci_value in [QRCode.ECI.ISO_8859_1, QRCode.ECI.SHIFT_JIS, QRCode.ECI.UTF_8, QRCode.ECI.UTF_16, QRCode.ECI.US_ASCII]:
                data_prop["type"] = TYPE_STRING
                data_prop["hint"] = PROPERTY_HINT_MULTILINE_TEXT
            else:
                data_prop["type"] = TYPE_PACKED_BYTE_ARRAY

    return [
        {
            "name": "_update_fn",
            "type": TYPE_CALLABLE,
            "usage": PROPERTY_USAGE_NONE if self.auto_update else PROPERTY_USAGE_EDITOR,
            "hint": PROPERTY_HINT_TOOL_BUTTON,
            "hint_string": "Update"
        },
        {
            "name": "auto_update",
            "type": TYPE_BOOL,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
        },
        {
            "name": "mode",
            "type": TYPE_INT,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
            "hint": PROPERTY_HINT_ENUM,
            "hint_string": "Numeric:1,Alphanumeric:2,Byte:4,Kanji:8"
        },
        {
            "name": "error_correction",
            "type": TYPE_INT,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
            "hint": PROPERTY_HINT_ENUM,
            "hint_string": "Low:1,Medium:0,Quartile:3,High:2"
        },
        {
            "name": "use_eci",
            "type": TYPE_BOOL,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
        },
        {
            "name": "eci_value",
            "type": TYPE_INT,
            "usage": (PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE) if self.use_eci else (PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY),
            "hint": PROPERTY_HINT_ENUM,
            "hint_string": "Code Page 437:2,ISO 8859-1:3,ISO 8859-2:4,ISO 8859-3:5,ISO 8859-4:6,ISO 8859-5:7,ISO 8859-6:8,ISO 8859-7:9,ISO 8859-8:10,ISO 8859-9:11,ISO 8859-10:12,ISO 8859-11:13,ISO 8859-12:14,ISO 8859-13:15,ISO 8859-14:16,ISO 8859-15:17,ISO 8859-16:18,Shift JIS:20,Windows 1250:21,Windows 1251:22,Windows 1252:23,Windows 1256:24,UTF-16:25,UTF-8:26,US ASCII:27,BIG 5:28,GB 18030:29,EUC KR:30"
        },
        data_prop,
        {
            "name": "auto_version",
            "type": TYPE_BOOL,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
        },
        {
            "name": "version",
            "type": TYPE_INT,
            "usage": (PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY) if self.auto_version else (PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE),
            "hint": PROPERTY_HINT_RANGE,
            "hint_string": "1,40"
        },
        {
            "name": "auto_mask_pattern",
            "type": TYPE_BOOL,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
        },
        {
            "name": "mask_pattern",
            "type": TYPE_INT,
            "usage": (PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY) if self.auto_mask_pattern else (PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE),
            "hint": PROPERTY_HINT_RANGE,
            "hint_string": "0,7"
        },
        {
            "name": "Appearance",
            "type": TYPE_NIL,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_GROUP,
        },
        {
            "name": "light_module_color",
            "type": TYPE_COLOR,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
        },
        {
            "name": "dark_module_color",
            "type": TYPE_COLOR,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
        },
        {
            "name": "auto_module_size",
            "type": TYPE_BOOL,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
        },
        {
            "name": "module_size",
            "type": TYPE_INT,
            "usage": (PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY) if self.auto_module_size else (PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE),
            "hint": PROPERTY_HINT_RANGE,
            "hint_string": "1,1,or_greater"
        },
        {
            "name": "quiet_zone_size",
            "type": TYPE_INT,
            "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
            "hint_string": "0,1,or_greater",
        },
    ]

func _validate_property(property: Dictionary) -> void:
    if property.name == "texture":
        property.usage &= ~(PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE)

func _property_can_revert(property: StringName) -> bool:
    return property in ["eci_value", "auto_version", "auto_mask_pattern", "light_module_color", "dark_module_color", "auto_module_size", "quiet_zone_size"]

func _property_get_revert(property: StringName) -> Variant:
    match property:
        "eci_value":
            return QRCode.ECI.ISO_8859_1
        "auto_version":
            return true
        "auto_mask_pattern":
            return true
        "light_module_color":
            return Color.WHITE
        "dark_module_color":
            return Color.BLACK
        "auto_module_size":
            return true
        "quiet_zone_size":
            return 4
        _:
            return null

func _get_configuration_warnings() -> PackedStringArray:
    if self.auto_module_size && self.expand_mode == EXPAND_KEEP_SIZE:
        return ["Do not use auto module px size AND keep size expand mode."]
    return []

func _notification(what: int) -> void:
    match what:
        NOTIFICATION_RESIZED:
            if self.auto_module_size && self.auto_update:
                self._update_texture()
            elif self.auto_module_size:
                self._update_module_size()

func _update_module_size() -> void:
    self.module_size = mini(self.size.x, self.size.y) / (self._qr.get_dimension() + 2 * self.quiet_zone_size)

func _update_texture() -> void:
    if self.auto_module_size:
        self._update_module_size()
    self.texture = ImageTexture.create_from_image(QRCode.generate_image(self._cached_data, self.module_size, self.light_module_color, self.dark_module_color, self.quiet_zone_size))
