#!/usr/bin/env python3

# Copyright 2021 Johannes Marbach
#
# This file is part of squeek2lvgl, hereafter referred to as the program.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import argparse
from typing import Set
import git
import os
import sys
import tempfile
import yaml


###
# Global constants

outfile_c = 'sq2lv_layouts.c'
outfile_h = 'sq2lv_layouts.h'

repository_url = 'https://gitlab.gnome.org/World/Phosh/squeekboard.git'
rel_layouts_dir = 'data/keyboards'


###
# General helpers
##

def die(msg):
    """Print an error message to STDERR and exit with a non-zero code.
    
    msg -- message to output on STDERR
    """
    sys.stderr.write(msg if msg.endswith('\n') else msg + '\n')
    sys.exit(1)


def warn(msg):
    """Print a warning message to STDERR.
    
    msg -- message to output on STDERR
    """
    sys.stderr.write(msg if msg.endswith('\n') else msg + '\n')


def parse_arguments():
    """ Parse commandline arguments.
    """
    parser = argparse.ArgumentParser(description='Convert squeekboard layouts to LVGL-compatible C code.')
    parser.add_argument('--input', dest='input', action='append', required=True, help='squeekboard layout to '
                        + 'use as input for generation. Has to be a YAML file path relative to data/keyboards. '
                        + 'Can be specified multiple times.')
    parser.add_argument('--name', dest='name', action='append', required=True, help='name for the layout. '
                        + 'Needs to be specified once for every --input flag.')
    parser.add_argument('--extra-top-row-base', dest='extra_top_row_base', type=str, required=False, help='additional '
                        + 'key row to add at the top of the base layer.')
    parser.add_argument('--extra-top-row-upper', dest='extra_top_row_upper', type=str, required=False, help='additional '
                        + 'key row to add at the top of the upper layer.')
    parser.add_argument('--shift-keycap', dest='shift_keycap', type=str, required=False, help='key caption for '
                        + 'the Shift key. Defaults to "Shift".')
    parser.add_argument('--surround-space-with-arrows', action='store_true', dest='arrows_around_space',
                        help='insert left / right arrow before / after space key')
    parser.add_argument('--generate-scancodes', action='store_true', dest='generate_scancodes', help='also '
                        + 'generate scancode tables (only works for US layout currently)')
    parser.add_argument('--output', dest='output', type=str, required=True, help='output directory for generated '
                        + 'files')
    args = parser.parse_args()

    if not args.output or not os.path.isdir(args.output):
        die('Error: no valid output directory specified')

    return args


def clone_squeekboard_repo(destination):
    """ Clone the squeekboard git repository.

    destination -- directory path to clone to
    """
    git.Repo.clone_from(repository_url, destination, depth=1)


def load_yaml(layouts_dir, rel_path):
    """ Load a YAML file and return its dictionary representation.
    
    rel_path -- path of the YAML file relative to the layouts root directory
    """
    path = os.path.join(layouts_dir, rel_path)
    if not os.path.isfile(path):
        die(f'could not find input file {path}')

    data = None
    with open(path, 'r') as stream:
        try:
            data = yaml.safe_load(stream)
        except yaml.YAMLError as exc:
            die(f'Could not load YAML file {path}: {exc}')
    if not data:
        die(f'could not load input file {path}')
    if not 'views' in data:
        die(f'no "views" element in YAML data loaded from input file {path}')
    
    return data


def write_files(lines_c, lines_h):
    """Write accumulated output to C and header file, respectively.

    lines_c -- sequence of lines to write to C file
    lines_h -- sequence of lines to write to header
    """
    with open(os.path.join(args.output, outfile_c), 'w') as fp:
        fp.write('\n'.join(lines_c))
    with open(os.path.join(args.output, outfile_h), 'w') as fp:
        fp.write('\n'.join(lines_h))


def comma_if_needed(sequence, idx):
    """Return a comma unless idx points to the last element in sequence.
    
    sequence -- a sequence of elements
    idx -- an index into the sequence
    """
    return ',' if idx < len(sequence) - 1 else ''


###
# SourceFileBuilder
##

class SourceFileBuilder(object):
    """Builder for .c and .h files.
    """

    def __init__(self):
        """Constructor.
        """
        self.lines = [] 
        self._add_header_comment()

    def add_line(self, line=None):
        """Add a single line and return the builder.
        
        line - string representing the line to add (if None, an empty line will be added)
        """
        self.lines.append(line if line else '')
        return self

    def add_lines(self, lines):
        """Add multiple lines and return the builder.
        
        lines - a list of strings representing the lines to add
        """
        self.lines += lines
        return self

    def wrap_in_ifndef(self, macro):
        """Wrap all of the current content in a macro check to prevent double inclusion and
        return the builder.
        
        macro -- name of the macro to use
        """
        idx = 0
        while self.lines[idx][:2] in ['/*', ' *']:
            idx += 1
        self.lines.insert(idx, '')
        self.lines.insert(idx + 1, f'#ifndef {macro}')
        self.lines.insert(idx + 2, f'#define {macro}')
        self.add_lines([f'#endif /* {macro} */', ''])

    def _add_header_comment(self):
        """Add the generator header comment and return the builder.
        """
        self.add_lines(['/**', ' * Auto-generated with squeek2lvgl', ' **/', ''])
        return self

    def add_include(self, header):
        """Add an include statement and return the builder.
        
        header -- path to file to be included
        """
        self.add_line(f'#include "{header}"')
        return self

    def add_system_include(self, header):
        """Add a system include statement and return the builder.
        
        header -- path to file to be included
        """
        self.add_line(f'#include <{header}>')
        return self

    def add_section_comment(self, title):
        """Add a comment marking the beginning of a section and return the builder.
        
        title -- title of the section
        """
        self.add_lines(['/**', f' * {title}', ' **/'])
        return self

    def add_subsection_comment(self, title):
        """Add a comment marking the beginning of a subsection and return the builder.
        
        title -- title of the subsection
        """
        self.add_line(f'/* {title} */')
        return self

    def add_array(self, static, type, identifier, values, row_terminator, array_terminator):
        """Add a row-based C array and return the builder.
        
        static -- True if the variable is static
        type -- variable type
        identifier -- variable identifier
        values -- values per row as a list of lists,
        row_terminator -- element to append to each row except the last
        array_terminator -- element to append to the last row
        """
        prefix = 'static ' if static else ''

        if not values or not values[0]:
            self.add_line(f'{prefix}{type} * const {identifier} = NULL;')
            return self

        self.add_line(f'{prefix}{type} {identifier}[] = ' + '{ \\')
        for i, values_in_row in enumerate(values):
            elements = values_in_row
            if i < len(values) - 1 and row_terminator:
                elements.append(row_terminator)
            if i == len(values) - 1 and array_terminator:
                elements.append(array_terminator)
            joined = ', '.join([f'{e}' for e in elements])
            self.add_line(f'    {joined}{comma_if_needed(values, i)} \\')
        self.add_line('};')

        return self

    def add_flat_array(self, static, type, identifier, values, array_terminator):
        """Add a flat C array and return the builder.
        
        static -- True if the variable is static
        type -- variable type
        identifier -- variable identifier
        values -- list of values,
        array_terminator -- element to append after the last element
        """
        return self.add_array(static, type, identifier, [values], '', array_terminator)


###
# Layout processing
##

def layout_id_to_c_identifier(layout_id):
    """Return a string for a layout that is suitable to be used in a C identifier.
    
    layout_id -- ID of the layout
    """
    return layout_id.lower().replace('/', '_')


layer_name_for_view_id = {
    'base': 'Lowercase letters',
    'upper': 'Uppercase letters',
    'numbers': 'Numbers / symbols',
    'eschars': 'Special characters',
    'symbols': 'Symbols',
    'actions': 'Actions'
}

def view_id_to_layer_name(view_id):
    """Return a descriptive name for a layer based on its view ID.
    
    view_id -- the ID of the view representing the layer
    """
    if view_id not in layer_name_for_view_id:
        return None
    return layer_name_for_view_id[view_id]


layer_identifier_for_view_id = {
    'base': 'lower',
    'eschars': 'special'
}

def view_id_to_c_identifier(view_id):
    """Return a string for a view that is suitable to be used in a C identifier.
    
    view_id -- ID of the view
    """
    return layer_identifier_for_view_id[view_id] if view_id in layer_identifier_for_view_id else view_id


ignored_keys = {
    'preferences'
}

def is_key_ignored(key):
    """Return True if a key should be ignored or False otherwise.
    
    key -- the key in question
    """
    return key in ignored_keys


keycap_for_key = {
    '\\': '\\\\',
    '"': '\\"',
    '↑': 'LV_SYMBOL_UP',
    '↓': 'LV_SYMBOL_DOWN',
    '←': 'LV_SYMBOL_LEFT',
    '→': 'LV_SYMBOL_RIGHT',
    'BackSpace': 'LV_SYMBOL_BACKSPACE',
    'colon': ':',
    'period': '.',
    'Shift_L': 'SQ2LV_SYMBOL_SHIFT',
    'space': ' ',
    'Return': 'LV_SYMBOL_OK',
    'Up': 'LV_SYMBOL_UP',
    'Left': 'LV_SYMBOL_LEFT',
    'Down': 'LV_SYMBOL_DOWN',
    'Right': 'LV_SYMBOL_RIGHT'
}

def key_to_keycap(args, key):
    """Return the keycap for a key.
    
    args -- commandline arguments
    key -- the key
    """
    return keycap_for_key[key] if key in keycap_for_key else key


def key_is_modifier(key, data_buttons):
    """Return true if a key acts as a modifier.

    key -- the key in question
    data_buttons -- the "buttons" object from the layout's YAML file
    """
    return key in data_buttons and 'modifier' in data_buttons[key]


repeatable_keys = {
    'BackSpace',
    'Del',
    'PgUp',
    'PgDn',
    'Return',
    'space',
    '↑',
    '←',
    '↓',
    '→',
    'Up',
    'Left',
    'Down',
    'Right'
}

def key_can_repeat(key):
    """Return True if a key can repeatedly emit while being held down.

    key -- the key
    """
    return key in repeatable_keys


def key_to_attributes(key, is_locked, is_lockable, is_extra_top_row, data_buttons):
    """Return the LVGL button attributes for a key.
    
    key -- the key in question
    is_locked - whether the key is locked  in the current view
    is_lockable - whether the key can be locked in the current view
    is_extra_top_row - whether the key is in the extra top row
    data_buttons -- the "buttons" object from the layout's YAML file
    """
    attributes = []

    if key_is_modifier(key, data_buttons):
        attributes.append('SQ2LV_CTRL_MOD_INACTIVE')
    elif is_locked:
        attributes.append('SQ2LV_CTRL_MOD_ACTIVE')
    elif is_lockable:
        attributes.append('SQ2LV_CTRL_MOD_INACTIVE')
    elif key in data_buttons and key not in ['"', 'colon', 'period', 'space'] or key in ['↑', '←', '↓', '→']:
        attributes.append('SQ2LV_CTRL_NON_CHAR')
    elif key not in ['space']:
        attributes.append('LV_BTNMATRIX_CTRL_POPOVER')

    if not key_can_repeat(key):
        attributes.append('LV_BTNMATRIX_CTRL_NO_REPEAT')

    if is_extra_top_row:
        attributes.append('SQ2LV_CTRL_NON_CHAR')
    if key == '<hidden>':
        attributes.append('LV_BTNMATRIX_CTRL_HIDDEN')

    if key not in data_buttons or key in ['"', 'colon', 'period']:
        attributes.append('2')
    elif key == 'space':
        attributes.append('7')
    else:
        attributes.append('3')

    return ' | '.join(attributes)


def keycap_to_c_value(keycap):
    """Return the right-hand side C value for a keycap
    """
    return keycap if keycap.startswith('LV_') or keycap.startswith('SQ2LV_') else f'"{keycap}"'


scancodes_for_keycap = {
    '0': ['KEY_0'],
    '1': ['KEY_1'],
    '2': ['KEY_2'],
    '3': ['KEY_3'],
    '4': ['KEY_4'],
    '5': ['KEY_5'],
    '6': ['KEY_6'],
    '7': ['KEY_7'],
    '8': ['KEY_8'],
    '9': ['KEY_9'],
    'a': ['KEY_A'],
    'b': ['KEY_B'],
    'c': ['KEY_C'],
    'd': ['KEY_D'],
    'e': ['KEY_E'],
    'f': ['KEY_F'],
    'g': ['KEY_G'],
    'h': ['KEY_H'],
    'i': ['KEY_I'],
    'j': ['KEY_J'],
    'k': ['KEY_K'],
    'l': ['KEY_L'],
    'm': ['KEY_M'],
    'n': ['KEY_N'],
    'o': ['KEY_O'],
    'p': ['KEY_P'],
    'q': ['KEY_Q'],
    'r': ['KEY_R'],
    's': ['KEY_S'],
    't': ['KEY_T'],
    'u': ['KEY_U'],
    'v': ['KEY_V'],
    'w': ['KEY_W'],
    'x': ['KEY_X'],
    'y': ['KEY_Y'],
    'z': ['KEY_Z'],
    'A': ['KEY_LEFTSHIFT', 'KEY_A'],
    'B': ['KEY_LEFTSHIFT', 'KEY_B'],
    'C': ['KEY_LEFTSHIFT', 'KEY_C'],
    'D': ['KEY_LEFTSHIFT', 'KEY_D'],
    'E': ['KEY_LEFTSHIFT', 'KEY_E'],
    'F': ['KEY_LEFTSHIFT', 'KEY_F'],
    'G': ['KEY_LEFTSHIFT', 'KEY_G'],
    'H': ['KEY_LEFTSHIFT', 'KEY_H'],
    'I': ['KEY_LEFTSHIFT', 'KEY_I'],
    'J': ['KEY_LEFTSHIFT', 'KEY_J'],
    'K': ['KEY_LEFTSHIFT', 'KEY_K'],
    'L': ['KEY_LEFTSHIFT', 'KEY_L'],
    'M': ['KEY_LEFTSHIFT', 'KEY_M'],
    'N': ['KEY_LEFTSHIFT', 'KEY_N'],
    'O': ['KEY_LEFTSHIFT', 'KEY_O'],
    'P': ['KEY_LEFTSHIFT', 'KEY_P'],
    'Q': ['KEY_LEFTSHIFT', 'KEY_Q'],
    'R': ['KEY_LEFTSHIFT', 'KEY_R'],
    'S': ['KEY_LEFTSHIFT', 'KEY_S'],
    'T': ['KEY_LEFTSHIFT', 'KEY_T'],
    'U': ['KEY_LEFTSHIFT', 'KEY_U'],
    'V': ['KEY_LEFTSHIFT', 'KEY_V'],
    'W': ['KEY_LEFTSHIFT', 'KEY_W'],
    'X': ['KEY_LEFTSHIFT', 'KEY_X'],
    'Y': ['KEY_LEFTSHIFT', 'KEY_Y'],
    'Z': ['KEY_LEFTSHIFT', 'KEY_Z'],
    'Alt': ['KEY_LEFTALT'],
    'Ctrl': ['KEY_LEFTCTRL'],
    'LV_SYMBOL_UP': ['KEY_UP'],
    'LV_SYMBOL_DOWN': ['KEY_DOWN'],
    'LV_SYMBOL_LEFT': ['KEY_LEFT'],
    'LV_SYMBOL_RIGHT': ['KEY_RIGHT'],
    'LV_SYMBOL_BACKSPACE': ['KEY_BACKSPACE'],
    'LV_SYMBOL_OK': ['KEY_ENTER'],
    ' ': ['KEY_SPACE'],
    'ABC': [],
    'abc': [],
    '123': [],
    'PgUp': ['KEY_PAGEUP'],
    'PgDn': ['KEY_PAGEDOWN'],
    'Home': ['KEY_HOME'],
    'End': ['KEY_END'],
    '*': ['KEY_LEFTSHIFT', 'KEY_8'],
    '#': ['KEY_LEFTSHIFT', 'KEY_3'],
    '$': ['KEY_LEFTSHIFT', 'KEY_4'],
    '/': ['KEY_SLASH'],
    '&': ['KEY_LEFTSHIFT', 'KEY_7'],
    '-': ['KEY_MINUS'],
    '_': ['KEY_LEFTSHIFT', 'KEY_MINUS'],
    '+': ['KEY_LEFTSHIFT', 'KEY_EQUAL'],
    '(': ['KEY_LEFTSHIFT', 'KEY_9'],
    ')': ['KEY_LEFTSHIFT', 'KEY_0'],
    ',': ['KEY_COMMA'],
    '\\"': ['KEY_LEFTSHIFT', 'KEY_APOSTROPHE'],
    '\'': ['KEY_APOSTROPHE'],
    ':': ['KEY_LEFTSHIFT', 'KEY_SEMICOLON'],
    ';': ['KEY_SEMICOLON'],
    '!': ['KEY_LEFTSHIFT', 'KEY_1'],
    '?': ['KEY_LEFTSHIFT', 'KEY_SLASH'],
    '.': ['KEY_DOT'],
    '~': ['KEY_LEFTSHIFT', 'KEY_GRAVE'],
    '`': ['KEY_GRAVE'],
    '|': ['KEY_LEFTSHIFT', 'KEY_BACKSLASH'],
    # '·': [],
    # '√': [],
    # 'π': [],
    # 'τ': [],
    # '÷': [],
    # '×': [],
    # '¶': [],
    # '©': [],
    # '®': [],
    # '£': [],
    # '€': [],
    # '¥': [],
    '\\\\': ['KEY_BACKSLASH'],
    '^': ['KEY_LEFTSHIFT', 'KEY_6'],
    # '°': [],
    '@': ['KEY_LEFTSHIFT', 'KEY_2'],
    '{': ['KEY_LEFTSHIFT', 'KEY_LEFTBRACE'],
    '}': ['KEY_LEFTSHIFT', 'KEY_RIGHTBRACE'],
    '%': ['KEY_LEFTSHIFT', 'KEY_5'],
    '<': ['KEY_LEFTSHIFT', 'KEY_COMMA'],
    '>': ['KEY_LEFTSHIFT', 'KEY_DOT'],
    '=': ['KEY_EQUAL'],
    '[': ['KEY_LEFTBRACE'],
    ']': ['KEY_RIGHTBRACE'],
    'F1': ['KEY_F1'],
    'F2': ['KEY_F2'],
    'F3': ['KEY_F3'],
    'F4': ['KEY_F4'],
    'F5': ['KEY_F5'],
    'F6': ['KEY_F6'],
    'F7': ['KEY_F7'],
    'F8': ['KEY_F8'],
    'F9': ['KEY_F9'],
    'F10': ['KEY_F10'],
    'F11': ['KEY_F11'],
    'F12': ['KEY_F12'],
    'Esc': ['KEY_ESC'],
    'Tab': ['KEY_TAB'],
    'Pause': ['KEY_PAUSE'],
    'Insert': ['KEY_INSERT'],
    'Del': ['KEY_DELETE'],
    'Menu': ['KEY_COMPOSE'],
    'Break': ['KEY_BREAK'],
    '↑': ['KEY_UP'],
    '←': ['KEY_LEFT'],
    '↓': ['KEY_DOWN'],
    '→': ['KEY_RIGHT']
}

def keycap_to_scancodes(args, keycap, is_switcher):
    """Return the scancodes needed to produce a keycap
    
    args -- commandline arguments
    keycap -- keycap to produce
    is_switcher -- whether the key is a layer switcher
    """
    if is_switcher:
        return []
    if keycap not in scancodes_for_keycap:
        warn(f'Cannot determine scancodes for unknown keycap "{keycap}"')
        return []
    return scancodes_for_keycap[keycap]


def get_keycaps_attrs_modifiers_switchers_scancodes(args, view_id, data_views, data_buttons, extra_top_row):
    """Return keycaps, LVGL button attributes, modifier key indexes, layer switching key indexes,
    layer switching key destinations and scancodes for a view
    
    args -- commandline arguments
    view_id -- ID of the view
    data_views -- the "views" object from the layout's YAML file
    data_buttons -- the "buttons" object from the layout's YAML file
    extra_top_row -- additional row of keys to insert at the top or "<hidden>" to insert an empty row
    """
    keycaps = []
    attrs = []
    modifier_idxs = []
    switcher_idxs = []
    switcher_dests = []
    scancodes = []

    idx = 0

    rows = data_views[view_id]
    if extra_top_row:
        rows = [extra_top_row] + rows

    for index, row in enumerate(rows):
        keycaps_in_row = []
        attrs_in_row = []
        scancodes_in_row = []

        keys = row.split()

        if args.arrows_around_space:
            space_idx = None
            try:
                space_idx = keys.index('space')
            except ValueError:
                pass
            if space_idx != None:
                keys.insert(space_idx, '←')
                keys.insert(space_idx + 2, '→')

        for key in keys:
            if is_key_ignored(key):
                continue

            keycap = None

            if key in data_buttons and 'label' in data_buttons[key] and key not in ['Up', 'Left', 'Down', 'Right']:
                keycap = data_buttons[key]['label'].replace('\\', '\\\\')
            else:
                keycap = key_to_keycap(args, key)

            if not keycap:
                continue

            keycaps_in_row.append(keycap_to_c_value(keycap))

            if key_is_modifier(key, data_buttons):
                modifier_idxs.append(idx)

            is_locked = False
            is_lockable = False
            is_switcher = False

            if key in data_buttons and 'action' in data_buttons[key]:
                action = data_buttons[key]['action']
                dest = None

                if 'set_view' in action:
                    dest = action['set_view']
                elif 'locking' in action and 'lock_view' in action['locking'] and 'unlock_view' in action['locking']:
                    if action['locking']['lock_view'] == view_id:
                        dest = action['locking']['unlock_view']
                        is_locked = True
                    else:
                        dest = action['locking']['lock_view']
                        is_lockable = True
                if dest:
                    switcher_idxs.append(idx)
                    switcher_dests.append(dest)
                    is_switcher = True

            attrs_in_row.append(key_to_attributes(key, is_locked, is_lockable, extra_top_row and index == 0, data_buttons))

            if args.generate_scancodes:
                scancodes_in_row.append(keycap_to_scancodes(args, keycap, is_switcher))

            idx += 1

        keycaps.append(keycaps_in_row)
        attrs.append(attrs_in_row)
        scancodes.append(scancodes_in_row)

    return keycaps, attrs, modifier_idxs, switcher_idxs, switcher_dests, scancodes


def flatten_scancodes(scancodes):
    """Process a nested list of scancodes per row and key and return a flattened list of scancodes per row,
    a list of starting indexes and a list of scancode counts.
    
    scancodes -- list (rows) of list (keys) of list (scancodes) of scancodes
    """
    flat = []
    idxs = []
    nums = []

    if args.generate_scancodes:
        idx = 0
        num = 0

        for scancodes_in_row in scancodes:
            flat_in_row = []
            idxs_in_row = []
            nums_in_row = []

            for codes in scancodes_in_row:
                flat_in_row += codes
                idxs_in_row.append(idx if len(codes) > 0 else -1)
                nums_in_row.append(len(codes))
                idx += len(codes)

            flat.append(flat_in_row)
            idxs.append(idxs_in_row)
            nums.append(nums_in_row)

    return flat, idxs, nums


###
# Main
##

if __name__ == '__main__':
    args = parse_arguments()

    c_builder = SourceFileBuilder()
    c_builder.add_include(outfile_h)
    if args.generate_scancodes:
        c_builder.add_system_include('linux/input.h')
    c_builder.add_line()

    shift_keycap = args.shift_keycap if args.shift_keycap else 'Shift'
    c_builder.add_line(f'#define SQ2LV_SYMBOL_SHIFT "{shift_keycap}"')
    c_builder.add_line()

    h_builder = SourceFileBuilder()
    h_builder.add_include('lvgl/lvgl.h')
    h_builder.add_line()
    h_builder.add_line(f'#define SQ2LV_SCANCODES_ENABLED {1 if args.generate_scancodes else 0}')
    h_builder.add_line()
    h_builder.add_subsection_comment('Key attributes')
    h_builder.add_line('#define SQ2LV_CTRL_NON_CHAR     (LV_BTNMATRIX_CTRL_CLICK_TRIG | LV_BTNMATRIX_CTRL_CHECKED)')
    h_builder.add_line('#define SQ2LV_CTRL_MOD_ACTIVE   (LV_BTNMATRIX_CTRL_CLICK_TRIG | LV_BTNMATRIX_CTRL_CHECKABLE)')
    h_builder.add_line('#define SQ2LV_CTRL_MOD_INACTIVE (LV_BTNMATRIX_CTRL_CLICK_TRIG | LV_BTNMATRIX_CTRL_CHECKABLE | LV_BTNMATRIX_CTRL_CHECKED)')
    h_builder.add_line()

    layouts = []
    unique_scancodes = {}

    with tempfile.TemporaryDirectory() as tmp:
        clone_squeekboard_repo(tmp)

        layouts_dir = os.path.join(tmp, rel_layouts_dir)

        for file, layout_name in zip(args.input, args.name):
            layout_id, _ = os.path.splitext(file)
            layout_identifier = layout_id_to_c_identifier(layout_id)

            data = load_yaml(layouts_dir, file)
            data_views = data['views']
            data_buttons = data['buttons'] if 'buttons' in data else {}

            c_builder.add_section_comment(f'Layout: {layout_name} - generated from {layout_id}')
            c_builder.add_line()
            c_builder.add_line(f'static const char * const name_{layout_identifier} = "{layout_name}";')
            c_builder.add_line(f'static const char * const short_name_{layout_identifier} = "{layout_id}";')
            c_builder.add_line()

            layer_identifiers = []

            view_ids = [view_id for view_id in data_views if view_id_to_layer_name(view_id) != None]

            for view_id in data_views:
                layer_name = view_id_to_layer_name(view_id)
                if not layer_name:
                    warn(f'Ignoring unknown view_id {view_id}')
                    continue

                layer_identifier = f'{view_id_to_c_identifier(view_id)}_{layout_identifier}'
                layer_identifiers.append(layer_identifier)

                c_builder.add_subsection_comment(f'Layer: {layer_name} - generated from {view_id}')
                c_builder.add_line()
 
                extra_top_row = None
                if view_id == "base":
                    extra_top_row = args.extra_top_row_base
                if view_id == "upper":
                    extra_top_row = args.extra_top_row_upper
                if not extra_top_row and (args.extra_top_row_base or args.extra_top_row_upper):
                    extra_top_row = "<hidden>"

                keycaps, attrs, modifier_idxs, switcher_idxs, switcher_dests, scancodes = get_keycaps_attrs_modifiers_switchers_scancodes(
                    args, view_id, data_views, data_buttons, extra_top_row)

                for dest in switcher_dests:
                    if dest not in view_ids:
                        die(f'Unhandled layer switch destination {dest}')
                switcher_dests = [view_ids.index(d) for d in switcher_dests if d in view_ids]

                c_builder.add_line(f'static const int num_keys_{layer_identifier} = {sum([len(row) for row in keycaps])};')
                c_builder.add_line()
                c_builder.add_array(True, 'const char * const', f'keycaps_{layer_identifier}', keycaps, '"\\n"', '""')
                c_builder.add_line()
                c_builder.add_array(True, 'const lv_btnmatrix_ctrl_t', f'attributes_{layer_identifier}', attrs, '', '')
                c_builder.add_line()

                c_builder.add_line(f'static const int num_modifiers_{layer_identifier} = {len(modifier_idxs)};')
                c_builder.add_line()
                c_builder.add_flat_array(True, 'const int', f'modifier_idxs_{layer_identifier}', modifier_idxs, '')
                c_builder.add_line()

                c_builder.add_line(f'static const int num_switchers_{layer_identifier} = {len(switcher_idxs)};')
                c_builder.add_line()
                c_builder.add_flat_array(True, 'const int', f'switcher_idxs_{layer_identifier}', switcher_idxs, '')
                c_builder.add_line()
                c_builder.add_flat_array(True, 'const int', f'switcher_dests_{layer_identifier}', switcher_dests, '')
                c_builder.add_line()

                if args.generate_scancodes:
                    scancodes_flat, scancode_idxs, scancode_nums = flatten_scancodes(scancodes)

                    for scancodes_in_row in scancodes_flat:
                        for scancode in scancodes_in_row:
                            unique_scancodes[scancode] = True

                    c_builder.add_line(f'static const int num_scancodes_{layer_identifier} = {len(scancodes)};')
                    c_builder.add_line()
                    c_builder.add_array(True, 'const int', f'scancodes_{layer_identifier}', scancodes_flat, '', '')
                    c_builder.add_line()
                    c_builder.add_array(True, 'const int', f'scancode_idxs_{layer_identifier}', scancode_idxs, '', '')
                    c_builder.add_line()
                    c_builder.add_array(True, 'const int', f'scancode_nums_{layer_identifier}', scancode_nums, '', '')
                    c_builder.add_line()

            c_builder.add_subsection_comment(f'Layer array')
            c_builder.add_line()
            c_builder.add_line(f'static const int num_layers_{layout_identifier} = {len(layer_identifiers)};')
            c_builder.add_line()
            c_builder.add_line(f'static const sq2lv_layer_t layers_{layout_identifier}[] = ' + '{')
            for i, identifier in enumerate(layer_identifiers):
                c_builder.add_line('    {')
                fields = ['num_keys', 'keycaps', 'attributes', 'num_modifiers', 'modifier_idxs', 'num_switchers', 'switcher_idxs', 'switcher_dests']
                if args.generate_scancodes:
                    fields += ['num_scancodes', 'scancodes', 'scancode_idxs', 'scancode_nums']
                for k, field in enumerate(fields):
                    c_builder.add_line(f'        .{field} = {field}_{identifier}{comma_if_needed(fields, k)}')
                c_builder.add_line('    }' + comma_if_needed(layer_identifiers, i))
            c_builder.add_line('};')
            c_builder.add_line()

            layouts.append({
                'name': layout_name,
                'short_name': layout_id,
                'identifier': layout_identifier
            })

    h_builder.add_line('/* Layout IDs, values can be used as indexes into the sq2lv_layouts array */')
    h_builder.add_line('typedef enum {')
    h_builder.add_line('    SQ2LV_LAYOUT_NONE = -1,')
    for i, layout in enumerate(layouts):
        identifier = layout['identifier'].upper()
        h_builder.add_line(f'    SQ2LV_LAYOUT_{identifier} = {i}{comma_if_needed(layouts, i)}')
    h_builder.add_line('} sq2lv_layout_id_t;')
    h_builder.add_line()

    h_builder.add_line('/* Layer type */')
    h_builder.add_line('typedef struct {')
    h_builder.add_line('    /* Number of keys */')
    h_builder.add_line('    const int num_keys;')
    h_builder.add_line('    /* Key caps */')
    h_builder.add_line('    const char * const * const keycaps;')
    h_builder.add_line('    /* Key attributes */')
    h_builder.add_line('    const lv_btnmatrix_ctrl_t * const attributes;')
    h_builder.add_line('    /* Number of modifier keys */')
    h_builder.add_line('    const int num_modifiers;')
    h_builder.add_line('    /* Button indexes of modifier keys */')
    h_builder.add_line('    const int * const modifier_idxs;')
    h_builder.add_line('    /* Number of buttons that trigger a layer switch */')
    h_builder.add_line('    const int num_switchers;')
    h_builder.add_line('    /* Button indexes that trigger a layer switch */')
    h_builder.add_line('    const int * const switcher_idxs;')
    h_builder.add_line('    /* Indexes of layers to jump to when triggering layer switch buttons */')
    h_builder.add_line('    const int * const switcher_dests;')
    if args.generate_scancodes:
        h_builder.add_line('    /* Total number of scancodes */')
        h_builder.add_line('    const int num_scancodes;')
        h_builder.add_line('    /* Flat array of scancodes */')
        h_builder.add_line('    const int * const scancodes;')
        h_builder.add_line('    /* Start index in scancodes array for key cap */')
        h_builder.add_line('    const int * const scancode_idxs;')
        h_builder.add_line('    /* Number of scancodes for key cap */')
        h_builder.add_line('    const int * const scancode_nums;')
    h_builder.add_line('} sq2lv_layer_t;')
    h_builder.add_line()

    h_builder.add_line('/* Layout type */')
    h_builder.add_line('typedef struct {')
    h_builder.add_line('    /* Layout name */')
    h_builder.add_line('    const char * const name;')
    h_builder.add_line('    /* Layout short name */')
    h_builder.add_line('    const char * const short_name;')
    h_builder.add_line('    /* Total number of layers */')
    h_builder.add_line('    const int num_layers;')
    h_builder.add_line('    /* Layers array */')
    h_builder.add_line('    const sq2lv_layer_t * const layers;')
    h_builder.add_line('} sq2lv_layout_t;')
    h_builder.add_line()

    h_builder.add_line('/* Layouts */')
    h_builder.add_line('extern const int sq2lv_num_layouts;')
    h_builder.add_line('extern const sq2lv_layout_t sq2lv_layouts[];')
    h_builder.add_line()

    h_builder.add_line('/* Layout names (suitable for use in lv_dropdown_t) */')
    h_builder.add_line('extern const char * const sq2lv_layout_names;') 
    h_builder.add_line('extern const char * const sq2lv_layout_short_names;') 
    h_builder.add_line()

    if args.generate_scancodes:
        h_builder.add_line('/* Unique scancodes from all layout (suitable for setting up uinput devices) */')
        h_builder.add_line('extern const int sq2lv_num_unique_scancodes;')
        h_builder.add_line('extern const int sq2lv_unique_scancodes[];')
        h_builder.add_line()

    c_builder.add_section_comment('Public interface')
    c_builder.add_line()

    c_builder.add_line('const int sq2lv_num_layouts = ' + str(len(layouts)) + ';')
    c_builder.add_line()

    c_builder.add_line('const sq2lv_layout_t sq2lv_layouts[] = {')
    for i, layout in enumerate(layouts):
        c_builder.add_line('    /* ' + layout['name'] + ' */')
        c_builder.add_line('    {')
        fields = ['name', 'short_name', 'num_layers', 'layers']
        identifier = layout['identifier']
        for j, field in enumerate(fields):
            c_builder.add_line(f'        .{field} = {field}_{identifier}{comma_if_needed(fields, j)}')
        c_builder.add_line('    }' + comma_if_needed(layouts, i))
    c_builder.add_line('};')
    c_builder.add_line()

    names = [layout['name'] for layout in layouts]
    names = '\n    ' + ' "\\n"\n    '.join([f'"{name}"' for name in names])
    c_builder.add_line(f'const char * const sq2lv_layout_names ={names};')
    c_builder.add_line()

    short_names = [layout['short_name'] for layout in layouts]
    short_names = '\n    ' + ' "\\n"\n    '.join([f'"{short_name}"' for short_name in short_names])
    c_builder.add_line(f'const char * const sq2lv_layout_short_names ={short_names};')
    c_builder.add_line()

    if args.generate_scancodes:
        c_builder.add_line(f'const int sq2lv_num_unique_scancodes = {len(unique_scancodes)};')
        c_builder.add_line()
        c_builder.add_line('const int sq2lv_unique_scancodes[] = {')
        scancodes = list(unique_scancodes.keys())
        chunks = [scancodes[i:i + 10] for i in range(0, len(scancodes), 10)]
        for i, chunk in enumerate(chunks):
            joined = ', '.join(chunk)
            c_builder.add_line(f'    {joined}{comma_if_needed(chunks, i)}')
        c_builder.add_line('};')
        c_builder.add_line()

    h_builder.wrap_in_ifndef('SQ2LV_LAYOUTS_H')

    write_files(c_builder.lines, h_builder.lines)