// MAX7219 Matrix framebuffer add-on

/*
{
  "id": "max7219_matrix_fb",
  "name": "MAX7219 LED Matrix",
  "description": "Control MAX7219 LED matrices (HW692/FC16) as a framebuffer display. Supports multiple 8x8 modules on the X and Y axes.",
  "version": "1.0.0",
  "author": "Milan Spacek + Codex",
  "tags": ["MAX7219", "LED matrix", "framebuffer", "display"],
  "screenshots": ["max7219_matrix.png"],
  "changelog": "v1.0.0 - First version"
}
*/

// MicroPython library embedded as a JS string
var max7219_matrix_lib = `
# --------------------------------------------
# ESP IDE  : FREE MicroPython WEB IDE
# AUTHOR   : Milan Spacek (2019-2026)
# WEB      : https://espide.eu
# LICENSE  : AGPL-3.0
#
# CODE IS OPEN - IMPROVEMENTS MUST STAY OPEN
# Please contribute your improvements back
# --------------------------------------------
#
# MAX7219 tiled matrix driver (FrameBuffer compatible)
# - supports X * Y modules (each module is 8x8)
# - uses 3-wire bitbang interface: CLK, DIN, CS
# - drawing API is inherited from framebuf.FrameBuffer

from micropython import const
from machine import Pin
import framebuf
import time

try:
    from machine import disable_irq, enable_irq
except Exception:
    def disable_irq():
        return None
    def enable_irq(_state):
        return None

_NOOP = const(0x00)
_DIGIT0 = const(0x01)
_DECODEMODE = const(0x09)
_INTENSITY = const(0x0A)
_SCANLIMIT = const(0x0B)
_SHUTDOWN = const(0x0C)
_DISPLAYTEST = const(0x0F)


class MAX7219Matrix(framebuf.FrameBuffer):
    def __init__(
        self,
        modules_x,
        modules_y,
        pin_clk,
        pin_din,
        pin_cs,
        buffer=None,
        layout="HW692",
        serpentine=False,
        reverse_chain=None,
        flip_x=False,
        flip_y=False,
        flip_modules_y=False,
        reverse_bits=False,
        bit_delay_us=1,
    ):
        self.modules_x = int(modules_x)
        self.modules_y = int(modules_y)
        if self.modules_x < 1 or self.modules_y < 1:
            raise ValueError("modules_x and modules_y must be >= 1")

        self.width = self.modules_x * 8
        self.height = self.modules_y * 8
        self.total_modules = self.modules_x * self.modules_y
        self._stride = self.width // 8

        size = self._stride * self.height
        if buffer is None:
            self.buffer = bytearray(size)
        else:
            if len(buffer) != size:
                raise ValueError("Buffer musi mit delku %d" % size)
            self.buffer = buffer

        self.clk = Pin(int(pin_clk), Pin.OUT, value=0)
        self.din = Pin(int(pin_din), Pin.OUT, value=0)
        self.cs = Pin(int(pin_cs), Pin.OUT, value=1)

        self.layout = str(layout).upper()
        self._serpentine = bool(serpentine)

        if reverse_chain is None:
            if self.layout in ("HW692", "FC16"):
                self._reverse_chain = True
            else:
                self._reverse_chain = False
        else:
            self._reverse_chain = bool(reverse_chain)

        self._flip_x = bool(flip_x)
        self._flip_y = bool(flip_y)
        self._flip_modules_y = bool(flip_modules_y)
        self._reverse_bits = bool(reverse_bits)

        if self.layout in ("HW692_ROT180", "FC16_ROT180"):
            self._flip_x = not self._flip_x
            self._flip_y = not self._flip_y
            self._reverse_bits = not self._reverse_bits

        self._bit_delay_us = int(bit_delay_us) if int(bit_delay_us) > 0 else 0
        self._chain = self._build_chain_map()

        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_HLSB)

        self.init()

    def set_mapping(
        self,
        reverse_chain=None,
        flip_x=None,
        flip_y=None,
        flip_modules_y=None,
        reverse_bits=None,
        serpentine=None,
    ):
        if reverse_chain is not None:
            self._reverse_chain = bool(reverse_chain)
        if flip_x is not None:
            self._flip_x = bool(flip_x)
        if flip_y is not None:
            self._flip_y = bool(flip_y)
        if flip_modules_y is not None:
            self._flip_modules_y = bool(flip_modules_y)
        if reverse_bits is not None:
            self._reverse_bits = bool(reverse_bits)
        if serpentine is not None:
            self._serpentine = bool(serpentine)
        self._chain = self._build_chain_map()

    def _build_chain_map(self):
        chain = []
        for my in range(self.modules_y):
            if self._serpentine and (my % 2 == 1):
                xs = range(self.modules_x - 1, -1, -1)
            else:
                xs = range(self.modules_x)
            for mx in xs:
                chain.append((mx, my))
        if self._reverse_chain:
            chain.reverse()
        return chain

    def _shift_byte(self, value):
        value &= 0xFF
        for bit in range(7, -1, -1):
            self.clk.value(0)
            self.din.value((value >> bit) & 1)
            if self._bit_delay_us:
                time.sleep_us(self._bit_delay_us)
            self.clk.value(1)
            if self._bit_delay_us:
                time.sleep_us(self._bit_delay_us)

    def _write_cmd_all(self, command, data):
        irq_state = disable_irq()
        try:
            self.cs.value(0)
            if self._bit_delay_us:
                time.sleep_us(self._bit_delay_us)
            for _ in range(self.total_modules):
                self._shift_byte(command)
                self._shift_byte(data)
            if self._bit_delay_us:
                time.sleep_us(self._bit_delay_us)
            self.cs.value(1)
        finally:
            enable_irq(irq_state)

    def _write_row(self, row):
        reg = _DIGIT0 + row
        irq_state = disable_irq()
        try:
            self.cs.value(0)
            if self._bit_delay_us:
                time.sleep_us(self._bit_delay_us)
            for mx, my in self._chain:
                if self._flip_y:
                    src_row = 7 - row
                else:
                    src_row = row
                src_my = (self.modules_y - 1 - my) if self._flip_modules_y else my
                y = (src_my * 8) + src_row
                src_mx = (self.modules_x - 1 - mx) if self._flip_x else mx
                data = self.buffer[(y * self._stride) + src_mx]
                if self._reverse_bits:
                    data = (
                        ((data & 0x01) << 7)
                        | ((data & 0x02) << 5)
                        | ((data & 0x04) << 3)
                        | ((data & 0x08) << 1)
                        | ((data & 0x10) >> 1)
                        | ((data & 0x20) >> 3)
                        | ((data & 0x40) >> 5)
                        | ((data & 0x80) >> 7)
                    )
                self._shift_byte(reg)
                self._shift_byte(data)
            if self._bit_delay_us:
                time.sleep_us(self._bit_delay_us)
            self.cs.value(1)
        finally:
            enable_irq(irq_state)

    def init(self):
        for command, data in (
            (_SHUTDOWN, 0),
            (_DISPLAYTEST, 0),
            (_SCANLIMIT, 7),
            (_DECODEMODE, 0),
            (_SHUTDOWN, 1),
            (_INTENSITY, 8),
        ):
            self._write_cmd_all(command, data)
        self.fill(0)
        self.show()

    def power(self, on=True):
        self._write_cmd_all(_SHUTDOWN, 1 if on else 0)

    def test(self, enabled=False):
        self._write_cmd_all(_DISPLAYTEST, 1 if enabled else 0)

    def brightness(self, value):
        value = int(value)
        if value < 0:
            value = 0
        if value > 15:
            value = 15
        self._write_cmd_all(_INTENSITY, value)

    def show(self):
        for row in range(8):
            self._write_row(row)

    def clear(self):
        self.fill(0)
        self.show()
`;

// Button for uploading the library to the processor
demoWorkspace.registerButtonCallback("upload_lib_file_max7219_matrix", async function(button) {
  if (!isEditorConnected()) {
    Swal.fire("Error", "Device is not connected!", "error");
    return;
  }

  try {
    mp().mute_terminal = true;
    term.writeln("Uploading file: max7219_matrix.py");
    await delay(50);

    await mp().sendFile("lib/max7219_matrix.py", max7219_matrix_lib, false);
    await delay(250);
    await mp().sendData('\r\n');
    await delay(250);
    mp().mute_terminal = false;
    await mp().sendData('\r\n');

    Swal.fire("Done", "File was uploaded to the ESP!", "success");
  } catch (e) {
    Swal.fire("Error", e.message, "error");
  }
});

// Library import for generated code
var max7219_matrix_import = `
import framebuf
try:
    import max7219_matrix
except:
    print(terminal_color('Library max7219_matrix.py was not found on the device.',col=91))
    print(terminal_color(' Please upload the library to the device with the button',col=91))
    print(terminal_color('        Upload library to processor',col=91))
`;

function boolToPy(value) {
  return value === 'TRUE' ? 'True' : 'False';
}

function triToPy(value) {
  if (value === 'TRUE') return 'True';
  if (value === 'FALSE') return 'False';
  return 'None';
}

// -----------------------
// Python code generator
// -----------------------

Blockly.Python['max7219_matrix_init'] = function(block) {
  Blockly.Python.definitions_['import_max7219_matrix'] = max7219_matrix_import;

  var modulesX = Blockly.Python.valueToCode(block, 'MODULES_X', Blockly.Python.ORDER_ATOMIC) || '4';
  var modulesY = Blockly.Python.valueToCode(block, 'MODULES_Y', Blockly.Python.ORDER_ATOMIC) || '1';
  var pinClk = Blockly.Python.valueToCode(block, 'PIN_CLK', Blockly.Python.ORDER_ATOMIC) || '3';
  var pinDin = Blockly.Python.valueToCode(block, 'PIN_DIN', Blockly.Python.ORDER_ATOMIC) || '0';
  var pinCs = Blockly.Python.valueToCode(block, 'PIN_CS', Blockly.Python.ORDER_ATOMIC) || '1';
  var delayUs = Blockly.Python.valueToCode(block, 'BIT_DELAY_US', Blockly.Python.ORDER_ATOMIC) || '2';
  var brightness = Blockly.Python.valueToCode(block, 'BRIGHTNESS', Blockly.Python.ORDER_ATOMIC) || '4';

  var layout = block.getFieldValue('LAYOUT') || 'HW692';
  var reverseChain = triToPy(block.getFieldValue('REVERSE_CHAIN') || 'AUTO');
  var serpentine = boolToPy(block.getFieldValue('SERPENTINE') || 'FALSE');
  var flipX = boolToPy(block.getFieldValue('FLIP_X') || 'FALSE');
  var flipY = boolToPy(block.getFieldValue('FLIP_Y') || 'FALSE');
  var flipModulesY = boolToPy(block.getFieldValue('FLIP_MODULES_Y') || 'FALSE');
  var reverseBits = boolToPy(block.getFieldValue('REVERSE_BITS') || 'FALSE');

  var code = '';
  code += 'modules_x = int(' + modulesX + ')\n';
  code += 'modules_y = int(' + modulesY + ')\n';
  code += 'width = modules_x * 8\n';
  code += 'height = modules_y * 8\n';
  code += 'buffer = bytearray((width // 8) * height)\n';
  code += 'fbuf = framebuf.FrameBuffer(buffer, width, height, framebuf.MONO_HLSB)\n';
  code += 'display = max7219_matrix.MAX7219Matrix(modules_x=modules_x, modules_y=modules_y, pin_clk=' + pinClk + ', pin_din=' + pinDin + ', pin_cs=' + pinCs + ', buffer=buffer, layout="' + layout + '", serpentine=' + serpentine + ', reverse_chain=' + reverseChain + ', flip_x=' + flipX + ', flip_y=' + flipY + ', flip_modules_y=' + flipModulesY + ', reverse_bits=' + reverseBits + ', bit_delay_us=' + delayUs + ')\n';
  code += 'display.brightness(' + brightness + ')\n';
  return code;
};

Blockly.Python['max7219_matrix_brightness'] = function(block) {
  Blockly.Python.definitions_['import_max7219_matrix'] = max7219_matrix_import;
  var value = Blockly.Python.valueToCode(block, 'VALUE', Blockly.Python.ORDER_ATOMIC) || '4';
  return 'display.brightness(' + value + ')\n';
};

Blockly.Python['max7219_matrix_set_mapping'] = function(block) {
  Blockly.Python.definitions_['import_max7219_matrix'] = max7219_matrix_import;
  var reverseChain = triToPy(block.getFieldValue('REVERSE_CHAIN') || 'AUTO');
  var serpentine = boolToPy(block.getFieldValue('SERPENTINE') || 'FALSE');
  var flipX = boolToPy(block.getFieldValue('FLIP_X') || 'FALSE');
  var flipY = boolToPy(block.getFieldValue('FLIP_Y') || 'FALSE');
  var flipModulesY = boolToPy(block.getFieldValue('FLIP_MODULES_Y') || 'FALSE');
  var reverseBits = boolToPy(block.getFieldValue('REVERSE_BITS') || 'FALSE');
  return 'display.set_mapping(reverse_chain=' + reverseChain + ', flip_x=' + flipX + ', flip_y=' + flipY + ', flip_modules_y=' + flipModulesY + ', reverse_bits=' + reverseBits + ', serpentine=' + serpentine + ')\n';
};

Blockly.Python['max7219_matrix_test'] = function(block) {
  Blockly.Python.definitions_['import_max7219_matrix'] = max7219_matrix_import;
  var state = boolToPy(block.getFieldValue('STATE') || 'TRUE');
  return 'display.test(' + state + ')\n';
};

Blockly.Python['max7219_matrix_power'] = function(block) {
  Blockly.Python.definitions_['import_max7219_matrix'] = max7219_matrix_import;
  var state = boolToPy(block.getFieldValue('STATE') || 'TRUE');
  return 'display.power(' + state + ')\n';
};

// -----------------------
// Block definitions
// -----------------------

Blockly.Blocks['max7219_matrix_init'] = {
  init: function() {
    this.appendDummyInput()
        .appendField('initialize MAX7219 matrix')
        .appendField(new Blockly.FieldDropdown([
          ['HW692 (FC16)', 'HW692'],
          ['HW692 rotated 180', 'HW692_ROT180'],
          ['FC16', 'FC16'],
          ['FC16 rotated 180', 'FC16_ROT180'],
          ['GENERIC', 'GENERIC']
        ]), 'LAYOUT');

    this.appendValueInput('MODULES_X')
        .setCheck('Number')
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField('modules X (in row)');

    this.appendValueInput('MODULES_Y')
        .setCheck('Number')
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField('modules Y (rows)');

    this.appendValueInput('PIN_CLK')
        .setCheck('Number')
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField('pin CLK');

    this.appendValueInput('PIN_DIN')
        .setCheck('Number')
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField('pin DIN');

    this.appendValueInput('PIN_CS')
        .setCheck('Number')
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField('pin CS');

    this.appendValueInput('BIT_DELAY_US')
        .setCheck('Number')
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField('bit delay [us]');

    this.appendValueInput('BRIGHTNESS')
        .setCheck('Number')
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField('brightness [0-15]');

    this.appendDummyInput()
        .appendField('reverse chain')
        .appendField(new Blockly.FieldDropdown([
          ['auto by layout', 'AUTO'],
          ['yes', 'TRUE'],
          ['no', 'FALSE']
        ]), 'REVERSE_CHAIN')
        .appendField('serpentine')
        .appendField(new Blockly.FieldDropdown([
          ['no', 'FALSE'],
          ['yes', 'TRUE']
        ]), 'SERPENTINE');

    this.appendDummyInput()
        .appendField('flip X')
        .appendField(new Blockly.FieldDropdown([
          ['no', 'FALSE'],
          ['yes', 'TRUE']
        ]), 'FLIP_X')
        .appendField('flip Y')
        .appendField(new Blockly.FieldDropdown([
          ['no', 'FALSE'],
          ['yes', 'TRUE']
        ]), 'FLIP_Y');

    this.appendDummyInput()
        .appendField('flip module rows Y')
        .appendField(new Blockly.FieldDropdown([
          ['no', 'FALSE'],
          ['yes', 'TRUE']
        ]), 'FLIP_MODULES_Y')
        .appendField('reverse bits')
        .appendField(new Blockly.FieldDropdown([
          ['no', 'FALSE'],
          ['yes', 'TRUE']
        ]), 'REVERSE_BITS');

    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#1f8a70');
    this.setTooltip('Initializes the MAX7219 framebuffer display and creates variables: width, height, buffer, fbuf, display.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['max7219_matrix_brightness'] = {
  init: function() {
    this.appendDummyInput()
        .appendField('MAX7219 set brightness');
    this.appendValueInput('VALUE')
        .setCheck('Number')
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField('0-15');
    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#1f8a70');
    this.setTooltip('Sets MAX7219 brightness from 0 to 15.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['max7219_matrix_set_mapping'] = {
  init: function() {
    this.appendDummyInput()
        .appendField('MAX7219 mapping');
    this.appendDummyInput()
        .appendField('reverse chain')
        .appendField(new Blockly.FieldDropdown([
          ['auto', 'AUTO'],
          ['yes', 'TRUE'],
          ['no', 'FALSE']
        ]), 'REVERSE_CHAIN')
        .appendField('serpentine')
        .appendField(new Blockly.FieldDropdown([
          ['no', 'FALSE'],
          ['yes', 'TRUE']
        ]), 'SERPENTINE');

    this.appendDummyInput()
        .appendField('flip X')
        .appendField(new Blockly.FieldDropdown([
          ['no', 'FALSE'],
          ['yes', 'TRUE']
        ]), 'FLIP_X')
        .appendField('flip Y')
        .appendField(new Blockly.FieldDropdown([
          ['no', 'FALSE'],
          ['yes', 'TRUE']
        ]), 'FLIP_Y');

    this.appendDummyInput()
        .appendField('flip module rows Y')
        .appendField(new Blockly.FieldDropdown([
          ['no', 'FALSE'],
          ['yes', 'TRUE']
        ]), 'FLIP_MODULES_Y')
        .appendField('reverse bits')
        .appendField(new Blockly.FieldDropdown([
          ['no', 'FALSE'],
          ['yes', 'TRUE']
        ]), 'REVERSE_BITS');

    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#1f8a70');
    this.setTooltip('Dynamic orientation/addressing change without reinitialization.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['max7219_matrix_test'] = {
  init: function() {
    this.appendDummyInput()
        .appendField('MAX7219 test mode')
        .appendField(new Blockly.FieldDropdown([
          ['on', 'TRUE'],
          ['off', 'FALSE']
        ]), 'STATE');
    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#1f8a70');
    this.setTooltip('Test mode: all LEDs ON/OFF.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['max7219_matrix_power'] = {
  init: function() {
    this.appendDummyInput()
        .appendField('MAX7219 power')
        .appendField(new Blockly.FieldDropdown([
          ['on', 'TRUE'],
          ['off', 'FALSE']
        ]), 'STATE');
    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#1f8a70');
    this.setTooltip('Software power on/off for the matrices.');
    this.setHelpUrl('');
  }
};

<!toolbox!>

<category name="MAX7219 Matrix" colour="#1f8a70">
  <label text="Before first use, upload the library to the processor."></label>
  <button text="Upload library to processor" callbackKey="upload_lib_file_max7219_matrix"></button>

  <block type="max7219_matrix_init">
    <value name="MODULES_X"><shadow type="math_number"><field name="NUM">4</field></shadow></value>
    <value name="MODULES_Y"><shadow type="math_number"><field name="NUM">1</field></shadow></value>
    <value name="PIN_CLK"><shadow type="math_number"><field name="NUM">3</field></shadow></value>
    <value name="PIN_DIN"><shadow type="math_number"><field name="NUM">0</field></shadow></value>
    <value name="PIN_CS"><shadow type="math_number"><field name="NUM">1</field></shadow></value>
    <value name="BIT_DELAY_US"><shadow type="math_number"><field name="NUM">2</field></shadow></value>
    <value name="BRIGHTNESS"><shadow type="math_number"><field name="NUM">4</field></shadow></value>
    <field name="LAYOUT">HW692</field>
    <field name="REVERSE_CHAIN">AUTO</field>
    <field name="SERPENTINE">FALSE</field>
    <field name="FLIP_X">FALSE</field>
    <field name="FLIP_Y">FALSE</field>
    <field name="FLIP_MODULES_Y">FALSE</field>
    <field name="REVERSE_BITS">FALSE</field>
  </block>

  <label text="For drawing, use the standard blocks from the Display category."></label>

  <label text="Diagnostics and orientation"></label>
  <block type="max7219_matrix_set_mapping">
    <field name="REVERSE_CHAIN">AUTO</field>
    <field name="SERPENTINE">FALSE</field>
    <field name="FLIP_X">FALSE</field>
    <field name="FLIP_Y">FALSE</field>
    <field name="FLIP_MODULES_Y">FALSE</field>
    <field name="REVERSE_BITS">FALSE</field>
  </block>
  <block type="max7219_matrix_brightness">
    <value name="VALUE"><shadow type="math_number"><field name="NUM">4</field></shadow></value>
  </block>
  <block type="max7219_matrix_test">
    <field name="STATE">TRUE</field>
  </block>
  <block type="max7219_matrix_power">
    <field name="STATE">TRUE</field>
  </block>
</category>
