# -*- coding: utf-8 -*-
"""
    rpi2caster is a CAT (computer-aided typesetting) software
    for the Monotype composition caster (a hot-metal typesetting machine).

    This project uses a control interface for 31/32 solenoid valves
    and a machine cycle sensor. It can control a casting machine
    or a pneumatic paper tape perforator from the Monotype keyboard.

    Machine control utility also serves as a diagnostic program
    for calibrating and testing the machine and control interface.

"""
from contextlib import suppress
from functools import partial, wraps
from zipfile import BadZipFile
import os

import click
import librpi2caster

# QR code generating backend
try:
    import qrcode
except ImportError:
    qrcode = None

# XLS sheet handling library
try:
    import openpyxl
except ImportError:
    openpyxl = None

from . import ui
from .functions import make_mat, make_galley, parse_ribbon
from .functions import pump_start, pump_stop
from .functions import single_justification, double_justification

# initialize the configuration for rpi2caster
USER_DATA_DIR = click.get_app_dir('rpi2caster', force_posix=True, roaming=True)
with suppress(FileExistsError):
    os.mkdir(USER_DATA_DIR, mode=0o775)

# pica = .1667, US pica = .166, Didot = .1776, Fournier = .1628
# make it configurable in the future
ui.PICA = 0.1776


def cast_this(ribbon_source):
    """Get the ribbon from decorated routine and cast it"""
    @wraps(ribbon_source)
    def wrapper(machine, *args, **kwargs):
        """Wrapper function"""
        ribbon = ribbon_source(*args, **kwargs)
        return machine.cast_or_punch(ribbon)
    return wrapper


@cast_this
def cast_xls(excel_file=None):
    """Cast characters specified in an Excel file"""
    def parse_xls(file):
        """Parse the Excel file interactively"""
        ui.display('Loading file: {}'.format(file.name))
        kwargs = dict(filename=file, read_only=True, data_only=True)
        workbook = openpyxl.load_workbook(**kwargs)
        sheet = workbook.active
        data = [[cell.value for cell in row] for row in sheet.rows]
        return data

    def read_sheet_data(data):
        """Make an order from sheet data"""
        def add_row(row):
            """Adds an entry to the queue or failed entries"""
            try:
                quantity, units = int(row[qty_col]), int(row[units_col])
                position = row[positions_col]
                char = row[chars_col]
                ui.display('Adding {} of {} at {} {} units wide.'
                           .format(quantity, char, position, units))
                matrix = make_mat(code=position, units=units,
                                  wedge=wedge, comment=char)
                mat_queue.append((matrix, quantity))
            except ValueError:
                failed_rows.append(row)

        width = max(len(row) for row in data)
        # data loaded - time for preview; show up to 10 rows
        preview_data = data[:min(10, len(data))]
        ui.display('Table preview:')
        for row in preview_data:
            ui.display('|'.join('{:10}'.format(v or '') for v in row))

        # ask about the data range
        starting_row = ui.enter('Starting row?', 2) - 1
        max_rows = len(data) - starting_row
        records = ui.enter('Number of rows to read?',
                           default=max_rows, maximum=max_rows)
        chars_col = ui.enter('Column # with characters?',
                             default=1, minimum=1, maximum=width) - 1
        positions_col = ui.enter('Column # with positions?',
                                 default=2, minimum=1, maximum=width) - 1
        units_col = ui.enter('Column # with units?',
                             default=3, minimum=1, maximum=width) - 1
        qty_col = ui.enter('Column # with type quantities?',
                           default=4, minimum=1, maximum=width) - 1
        # store all rows where error occured
        failed_rows = []
        for row in data[starting_row:starting_row+records]:
            add_row(row)

        if failed_rows:
            ui.display('Entries NOT being cast:')
            for row in failed_rows:
                ui.display('|'.join('{:10}'.format(v) for v in row))

    # choose the normal wedge
    wedge = ui.choose_wedge()
    mat_queue = []
    # load a spreadsheet
    file = excel_file or [ui.import_file(binary=True)]
    try:
        raw_data = parse_xls(file)
        read_sheet_data(raw_data)
    except (openpyxl.utils.exceptions.InvalidFileException,
            BadZipFile):
        ui.pause('{} is not a valid Excel 2007+ file, not reading...'
                 .format(file.name))

    # nothing to cast?
    if not mat_queue:
        return []
    chunk_size = ui.enter('How many sorts per group? '
                          '(0 = not separating with quads)',
                          default=10, minimum=0, maximum=20)
    # how wide is the galley?
    galley_inches = ui.choose_type_size('Galley width?', '25P')
    galley_units = wedge.inches_to_units(galley_inches)
    # generate the ribbon
    return make_galley(mat_queue, galley_units, wedge,
                       chunk_size, chunk_size)


@cast_this
def cast_material():
    """Cast typesetting material: typecases, specified sorts, spaces"""
    def make_queue():
        """generate a sequence of items for casting"""
        def add_new_chars():
            """Adds a mat to a queue"""
            while True:
                mat = ui.choose_mat(wedge, specify_units=True)
                if not mat:
                    return
                # how many matrices? (mat not specified? then don't ask)
                qty_prompt = 'How many sorts? (0 = cancel, start new)'
                qty = ui.enter(qty_prompt, default=10, minimum=0)
                if qty:
                    queue.append((mat, qty))
                    ui.display('Added {} of {}, {} units wide.'
                               .format(qty, mat.code, mat.units))
                else:
                    return

        def view_queue():
            """Show all characters in the queue"""
            items = ['{} of {}, {} units wide'
                     .format(qty, mat.code, mat.units)
                     for (mat, qty) in queue]
            ui.display('\n'.join(items))

        def delete_last_item():
            """Remove the last item added"""
            mat, _ = queue.pop()
            ui.display('Deleted {}'.format(mat.code))

        def cast_it():
            """Set a flag to finish specifying characters"""
            nonlocal done
            done = True

        queue = []
        done = False
        # menu for user decision
        options = [ui.option(key='a', value=add_new_chars, seq=1,
                             text='add characters'),
                   ui.option(key='d', value=delete_last_item, seq=2,
                             text='delete last entry'),
                   ui.option(key='v', value=view_queue, seq=3,
                             text='view queue'),
                   ui.option(key='c', value=cast_it, text='cast', seq=4)]

        while not done:
            prompt = 'Choose what to do:'
            ui.simple_menu(prompt, options, default_key='n')()

        # make a flat list of mats to cast from
        return queue

    wedge = ui.choose_wedge()
    chunk_size = ui.enter('How many sorts per group? '
                          '(0 = not separating with quads)',
                          default=10, minimum=0, maximum=20)
    mat_queue = make_queue()
    # nothing to cast?
    if not mat_queue:
        return []
    chunk_size = ui.enter('How many sorts per group? '
                          '(0 = not separating with quads)',
                          default=10, minimum=0, maximum=20)
    # how wide is the galley?
    galley_inches = ui.choose_type_size('Galley width?', '25P')
    galley_units = wedge.inches_to_units(galley_inches)
    # generate the ribbon
    return make_galley(mat_queue, galley_units, wedge,
                       chunk_size, chunk_size)


@cast_this
def cast_qr_code(input_data=''):
    """Set up and cast a QR code which can be printed and then scanned
    with a mobile device."""
    def make_qr(data):
        """make a QR code matrix from data"""
        # QR rendering parameters
        border = ui.enter('QR code border width (squares)?', default=1,
                          minimum=1, maximum=10)
        ec_option = ui.enter('Error correction: 0 = lowest, 3 = highest?',
                             default=1, minimum=0, maximum=3)
        # set up a QR code and generate a matrix
        modes = (qrcode.constants.ERROR_CORRECT_L,
                 qrcode.constants.ERROR_CORRECT_M,
                 qrcode.constants.ERROR_CORRECT_H,
                 qrcode.constants.ERROR_CORRECT_Q)
        engine = qrcode.QRCode(error_correction=modes[ec_option],
                               border=border)
        engine.add_data(data)
        qr_matrix = engine.get_matrix()
        return qr_matrix

    def render(pattern, high_space, low_space):
        """translate a pattern into Monotype control codes,
        applying single justification if space widths differ,
        making spaces square in shape"""
        characters = {False: low_space, True: high_space}
        # pump stop and last line out (this will be cast at the end)
        ribbon = [*pump_stop(), *double_justification()]

        for line in pattern:
            # predict the next character
            pairs = zip(line, [*line[1:], None])
            for curr_item, next_item in pairs:
                # add all spaces in a row
                curr_mat = characters.get(curr_item)
                next_mat = characters.get(next_item, curr_mat)
                ribbon.append(curr_mat.code)
                # check wedge positions and predict if we need to set them
                if curr_mat.wedges not in ((3, 8), next_mat.wedges):
                    # set the wedges only if we need to
                    # use single justification in this case
                    ribbon.extend(single_justification(curr_mat.wedges))
            # finish the line with double justification/galley trip
            # will be cast in reverse (i.e. starting with 0075+0005)
            ribbon.extend(double_justification(curr_mat.wedges))
        return ribbon

    # first choose a normal wedge for unit calculations
    wedge = ui.choose_wedge()

    # set the pixel size; smaler is preferred; the same as mould size
    # all pixels will be square; unit correction is used
    inches = ui.choose_type_size('Type size i.e. mould to use?')
    units = wedge.inches_to_units(inches)
    ui.display('The pixel size is {}" - that is {} units {} set.'
               .format(inches, units, wedge.set_width))

    # enter text and encode it
    qr_matrix = make_qr(input_data or ui.enter('Enter data to encode', ''))
    # let the operator know how large the code is
    size = len(qr_matrix)
    prompt = ('The resulting QR code is {0} × {0} squares '
              'or {1} × {1} inches.')
    ui.display(prompt.format(size, round(size * inches, 1)))
    ui.pause('Set your galley accordingly or abort.', allow_abort=True)
    # what matrix coordinates to use as low/high space?
    ui.display('Now choose the matrices to use for printed '
               'and for non-printed squares.\n\n'
               'It is recommended to use a high space at N15 '
               'and a low space at O15 respectively.\n')

    ui.display('Where is the high quad mat for printed squares?')
    high = ui.choose_mat(wedge, code='N15', units=units)
    ui.display('Where is the low quad mat for whitespace?')
    low = ui.choose_mat(wedge, code='O15', units=units)

    # make a sequence of low and high spaces to cast
    return render(qr_matrix, high, low)


@cast_this
def cast_diecase_proof():
    """Tests the whole diecase, casting from each matrix.
    Casts spaces between characters to be sure that the resulting
    type will be of equal width."""
    def make_diecase():
        """let the user choose the matrix case format,
        return lists of rows and columns to iterate over"""
        rows_15 = [n+1 for n in range(15)]
        rows_16 = [*rows_15, 16]
        cols_15 = [l for l in 'ABCDEFGHIJKLMNO']
        cols_17 = ['NI', 'NL', *cols_15]
        # ask the user about the diecase format
        options = [ui.option(key='1', value=(rows_15, cols_15),
                             text='15x15 small'),
                   ui.option(key='2', value=(rows_15, cols_17),
                             text='15x17 extended'),
                   ui.option(key='3', value=(rows_16, cols_17),
                             text='16x17 HMN, KMN, unit-shift')]
        prompt = 'Choose the matrix case size:'
        diecase = ui.simple_menu(prompt, options, default_key='2')
        # will return the lists of rows (15 or 16) and columns (15 or 17)
        return diecase

    # early cancel
    ui.display('This will cast a matrix case layout.\n'
               'You need to place a space at G2 and O15. \n'
               'It is recommended to use high space mats here, '
               'as the space may be used for supporting \n'
               'the overhanging characters, if they are too wide '
               'for the row they are placed, or a wrong wedge is used.')
    if not ui.confirm('Proceed?', default=True, abort_answer=False):
        return None

    # choose a wedge for casting the diecase proof
    wedge = ui.choose_wedge()
    rows, columns = make_diecase()
    # double pump stop and galley trip
    ribbon = [*pump_stop(), *double_justification()]
    # make a ribbon with all positions
    for row in rows:
        ui.display('Processing row {}'.format(row))
        # use G2 for narrower than 16 units and O15 for wide spaces
        # will be adjusted with variable wedges
        space_units = 23 - wedge[row]
        space_code = 'G2' if space_units < 16 else 'O15'
        space = make_mat(space_code, space_units, wedge)
        # line starting quad (in print) - cast last
        ribbon.append('O15')
        for column in columns:
            # add a space that will be cast after the character,
            # left of it (in print) b/c width adjustment
            # takes place on the left side of the character
            ribbon.append(space.code)
            # and then a character
            ribbon.append('{} {}'.format(column, row))
        # line ending quad (in print) - cast first
        ribbon.append('O15')
        # end line (cast first), set the wedges
        ribbon.extend(double_justification(space.wedges))

    return ribbon


def calibrate_machine(machine):
    """Casts the "en dash" characters for calibrating the character X-Y
    relative to type body."""
    def generate_ribbon():
        """Gets two mats for a given char and adjusts its parameters"""
        # specify the mats
        quad = ui.choose_mat(wedge, 'O15', 18,
                             char='a quad i.e. 18-unit space')
        space = ui.choose_mat(wedge, 'G5', 9,
                              char='a half-quad i.e. 9-unit space')
        char = ui.choose_mat(wedge, specify_units=True,
                             char='a calibration character, n or h')
        dash = ui.choose_mat(wedge, specify_units=True,
                             char='a dash or hyphen')

        valid_mats = [mat for mat in [quad, space, char, dash] if mat]
        return make_galley(valid_mats, wedge=wedge,
                           chunk_size=2, separate=False)

    if ui.confirm('Calibrate the bridge first?'):
        calibrate_bridge(machine)

    # Character width and position calibration
    ui.display('Mould blade opening width and X-Y character calibration:\n'
               'Measure the quad / half-quad width and adjust '
               'the mould blade abutment slide adjusting screw c14C1 \n'
               '(coarse) and micrometer wedge adjusting screw a20D2 (fine)'
               ' so that the type is of proper width.\n'
               '\nThen use the 33A1 / 33A2 micrometer screws on the bridge'
               ' to adjust the X-Y position of the character.\n'
               'Finally, put two dashes next to each other '
               '(one of them upside down) and adjust the Y position '
               'so that they line up.\n')

    if ui.confirm('Calibrate the mould and diecase?'):
        # go on, choose a wedge for calibration, widths depend on it
        wedge = ui.choose_wedge()
        ribbon = generate_ribbon()
        machine.advanced_cast(ribbon)


def calibrate_bridge(machine):
    """Calibrate the bridge draw rods to eliminate the diecase wobble"""
    ui.display('The diecase will be put in a central position (G8).\n'
               'Turn the machine to 350°, calibrate the bridge '
               'bushings a4A2 (3 / 2 paper adjustment).\n\n'
               'Turn the machine by hand and adjust the bridge draw rods '
               'so that the diecase is not wobbling anymore.\n'
               'Then check with the motor turned on.')
    ui.pause(allow_abort=True)
    machine.test(['G8'])


def diagnostics(machine):
    """Settings and alignment menu for servicing the caster"""
    def test_front_pinblock():
        """Sends signals 1...14, one by one"""
        info = 'Testing the front pinblock - signals 1...14.'
        ui.pause(info, allow_abort=True)
        machine.test([str(n) for n in range(1, 15)])

    def test_rear_pinblock():
        """Sends NI, NL, A...N"""
        info = 'This will test the rear pinblock - NI, NL, A...N. '
        ui.pause(info, allow_abort=True)
        machine.test(['NI', 'NL', *'ABCDEFGHIJKLMN'])

    def test_all():
        """Tests all valves and composition caster's inputs in original
        Monotype order:
        NMLKJIHGFSED 0075 CBA 123456789 10 11 12 13 14 0005.
        """
        info = ('This will test all the air lines in the same order '
                'as the holes on the paper tower: \n{}\n'
                'MAKE SURE THE PUMP IS DISENGAGED.')
        signals = [*'ONMLKJIHGFSED', '0075', *'CBA',
                   *(str(x) for x in range(1, 15)), '0005', '15']
        ui.pause(info.format(' '.join(signals)), allow_abort=True)
        machine.test(signals)

    def test_justification():
        """Tests the 0075-S-0005"""
        info = 'This will test the justification pinblock: 0075, S, 0005.'
        ui.pause(info, allow_abort=True)
        machine.test(['0075', 'S', '0005'])

    def test_any_code():
        """Tests a user-specified combination of signals"""
        while True:
            ui.display('Enter the signals to send to the caster, '
                       'or leave empty to return to menu: ')
            signals = ui.enter('Signals?', default=ui.Abort)
            machine.test([signals])

    def blow_all():
        """Blow all signals for a short time; add NI, NL also"""
        info = 'Blowing air through all air pins on both pinblocks...'
        ui.pause(info, allow_abort=True)
        queue = ['NI', 'NL', 'A1', 'B2', 'C3', 'D4', 'E5', 'F6', 'G7',
                 'H8', 'I9', 'J10', 'K11', 'L12', 'M13', 'N14',
                 '0075 S', '0005 O 15']
        machine.test(queue, duration=0.3)

    def calibrate_wedges():
        """Allows to calibrate the justification wedges so that when you're
        casting a 9-unit character with the S-needle at 0075:3 and 0005:8
        (neutral position), the    width is the same.

        It works like this:
        1. 0075 - turn the pump on,
        2. cast 7 spaces from the specified matrix (default: G5),
        3. put the line to the galley & set 0005 to 8, 0075 to 3, pump on,
        4. cast 7 spaces with the S-needle from the same matrix,
        5. put the line to the galley, then 0005 to turn the pump off.
        """
        ui.display('Transfer wedge calibration:\n\n'
                   'This function will cast two lines of 5 spaces: '
                   'first: G5, second: GS5 with wedges at 3/8. \n'
                   'Adjust the 52D space transfer wedge '
                   'until the lengths are the same.\n')
        ui.confirm('Proceed?', default=True, abort_answer=False)
        # prepare casting sequence
        record, justified_record = 'G7', 'GS7'
        pump_on, pump_off = 'NKS 0075 3', 'NJS 0005 8'
        line_out = 'NKJS 0005 0075 8'
        # start - 7 x G5 - line out - start - 7 x GS5 - line out - stop
        sequence = [pump_on, *[record] * 7, line_out, pump_on,
                    *[justified_record] * 7, line_out, pump_off]
        machine.simple_cast(sequence)

    def test_row_16():
        """Tests the row 16 addressing attachment (HMN, KMN, unit-shift).
        Casts from all matrices in 16th row.
        """
        ui.display('This will test the 16th row addressing.\n'
                   'If your caster has HMN, KMN or unit-shift attachment, '
                   'turn it on.\n')
        # build casting queue
        row = ['{}16'.format(col)
               for col in ('NI', 'NL', *'ABCDEFGHIJKLMNO')]
        # test with actual casting or not?
        if ui.confirm('Use the pump? Y = cast the row, N = test codes.'):
            sequence = [*pump_start(), *row,
                        *double_justification(), *pump_stop()]
            machine.simple_cast(sequence)
        else:
            machine.choose_row16_mode(row16_needed=True)
            machine.test(row)

    options = [ui.option(key='a', value=test_all, seq=1,
                         text='Test outputs',
                         desc='Test all air outputs N...O15, one by one'),

               ui.option(key='f', value=test_front_pinblock, seq=2,
                         text='Test the front pin block',
                         desc='Test the pins 1...14'),

               ui.option(key='r', value=test_rear_pinblock, seq=2,
                         text='Test the rear pin block',
                         desc='Test the pins NI, NL, A...N, one by one'),

               ui.option(key='b', value=blow_all, seq=2,
                         text='Blow all air pins',
                         desc='Blow air into every pin for a short time'),

               ui.option(key='j', value=test_justification, seq=2,
                         text='Test the justification block',
                         desc='Test the pins for 0075, S and 0005'),

               ui.option(key='c', value=test_any_code, seq=1,
                         text='Send specified signal combination',
                         desc='Send the specified signals to the machine'),

               ui.option(key='w', value=calibrate_wedges, seq=4,
                         cond=not machine.punch_mode,
                         text='Calibrate the 52D wedge',
                         desc=('Calibrate the space transfer wedge '
                               'for correct width')),

               ui.option(key='l', value=test_row_16, seq=5,
                         cond=not machine.punch_mode,
                         text='Test the extended 16x17 diecase system',
                         desc=('Cast type from row 16 '
                               'with HMN, KMN or unit-shift'))]

    header = 'Diagnostics and machine calibration menu:'
    catch_exceptions = (ui.Abort, KeyboardInterrupt, EOFError,
                        librpi2caster.MachineStopped)
    # Keep displaying the menu and go back here after any method ends
    ui.dynamic_menu(options=options, header=header,
                    catch_exceptions=catch_exceptions)


@click.group(invoke_without_command=True, help=__doc__,
             context_settings=dict(help_option_names=['-h', '--help']))
@click.version_option(None, '--version', '-V')
@click.option('verbosity', '-v', count=True, default=0,
              help='verbose mode (count, default=0)')
@click.option('--address', '-a', help='address (default: 127.0.0.1)',
              default='127.0.0.1')
@click.option('--port', '-p', help='port (default: 23017)', type=int,
              default=23017)
@click.option('excelfile', '-x', help='Excel file to cast from',
              type=click.File(mode='rb'))
@click.option('--qrcode', '-q', help='text to make a QR code from')
@click.option('--wedge', '-w', help='normal wedge designation, e.g. S5-12E')
@click.option('--diecaseproof', '-d', is_flag=True, help='cast diecase proof')
@click.option('--service', '-s', is_flag=True, help='service/diagnostics menu')
@click.option('--calibrate', '-c', is_flag=True,
              help='calibrate the machine')
@click.argument('ribbon', metavar='[filename]', type=click.File(),
                required=False)
@click.pass_context
def cli(ctx, verbosity, **args):
    """decide whether to go to a subcommand or enter main menu"""
    ui.verbosity = verbosity
    if not ctx.invoked_subcommand:
        ctx.forward(cast)


@cli.command('cast', hidden=True)
def cast(**attrs):
    """Cast type with a Monotype caster.

    Casts composition, material foargsr handsetting, QR codes.
    Can also cast a diecase proof.

    Can also be run in simulation mode without the actual caster."""
    @cast_this
    def cast_composition():
        """Casts or punches the ribbon contents if there are any"""
        with suppress(AttributeError):
            return ribbon.contents

    def main_menu():
        """Main menu for the type casting utility."""
        def choose_ribbon(*_):
            """Import the ribbon from a file"""
            nonlocal ribbon
            ribbon = ui.choose_ribbon()

        def display_ribbon(*_):
            """Display the ribbon contents"""
            ui.paged_display(ribbon.contents, sep='')

        def display_details(*_):
            """Collect ribbon, diecase and wedge data here"""
            data = [ribbon.parameters if ribbon else {}, machine.parameters]
            ui.display_parameters(*data)
            ui.pause()

        def options():
            """Generate options based on current state of the program."""
            is_punching = machine.punch_mode
            is_casting = not is_punching

            got_ribbon = bool(ribbon)

            ret = [ui.option(key='m',
                             value=calibrate_machine, seq=5,
                             cond=is_casting, text='Calibrate type',
                             desc='Align the character width, then diecase'),

                   ui.option(key='b', value=calibrate_bridge, seq=6,
                             cond=is_casting, text='Calibrate bridge',
                             desc='Align the diecase draw rods'),

                   ui.option(key='c', value=cast_composition, seq=10,
                             cond=is_casting and got_ribbon,
                             text='Cast composition',
                             desc='Cast type from a selected ribbon'),

                   ui.option(key='r', value=choose_ribbon, seq=10,
                             text='Select ribbon',
                             desc='Select a ribbon from database or file'),

                   ui.option(key='p', value=cast_composition, seq=30,
                             cond=is_punching and got_ribbon,
                             text='Punch ribbon',
                             desc='Punch a paper ribbon for casting'),

                   ui.option(key='v', value=display_ribbon, seq=80,
                             text='View codes', cond=got_ribbon,
                             desc='Display all codes in the selected ribbon'),

                   ui.option(key='h', value=cast_material, seq=60,
                             cond=is_casting, text='Cast sorts or spaces',
                             desc='Cast characters from selected mats'),

                   ui.option(key='x', value=cast_xls, seq=60,
                             cond=is_casting and openpyxl,
                             text='Cast type from specification',
                             desc='Cast characters specified in an XLS file'),

                   ui.option(key='q', value=cast_qr_code, seq=70,
                             cond=qrcode, text='Cast QR codes',
                             desc='Cast QR codes from high and low spaces'),

                   ui.option(key='F5', value=display_details, seq=92,
                             text='Show details...',
                             desc='Display ribbon and interface information'),

                   ui.option(key='F6', value=cast_diecase_proof, seq=93,
                             text='Diecase proof',
                             desc='Cast every character from the diecase'),

                   ui.option(key='F8', value=diagnostics, seq=95,
                             text='Diagnostics menu...',
                             desc='Interface and machine diagnostics')]
            return ret

        header = ('rpi2caster - CAT (Computer-Aided Typecasting) '
                  'for Monotype Composition or Type and Rule casters.\n\n'
                  'This program reads a ribbon (from file or database) '
                  'and casts the type on a composition caster.'
                  '\n\nCasting / Punching Menu:')
        exceptions = (ui.Abort, KeyboardInterrupt, EOFError)
        ui.dynamic_menu(options, header, catch_exceptions=exceptions,
                        func_args=(machine, ))

    address, port = attrs.get('address'), attrs.get('port')
    from .monotype import caster_factory
    machine = caster_factory(address, port)
    ribbon = parse_ribbon(attrs.get('ribbon'))
    # wedge = Wedge(args.get('wedge'))

    excel_file = attrs.get('excelfile')
    qr_code = attrs.get('qrcode')
    diecase_proof = attrs.get('diecase-proof')
    service = attrs.get('service')
    calibration = attrs.get('calibration')
    if ribbon:
        cast_composition(machine)
    elif excel_file:
        cast_xls(machine, excel_file)
    elif qr_code:
        cast_qr_code(machine, qr_code)
    elif diecase_proof:
        cast_diecase_proof(machine)
    elif service:
        diagnostics(machine)
    elif calibration:
        calibrate_machine(machine)
    else:
        main_menu()


@cli.command(options_metavar='[-ht]')
@click.option('--testing', '-t', is_flag=True, flag_value=True,
              help='use a unstable/development version instead of stable')
def update(testing):
    """Update the software."""
    # Upgrade routine
    dev_prompt = 'Testing version (newest features, but unstable)? '
    if ui.confirm('Update the software?', default=False):
        use_dev_version = testing or ui.confirm(dev_prompt, default=False)
        pre = '--pre' if use_dev_version else ''
        os.system('pip3 install {} --upgrade rpi2caster'.format(pre))


@cli.command(hidden=True)
def meow():
    "Easter egg."
    text = r"""
    ╔═══════════════════════════════════════════════════════════════════════╗
    ║                                   _                              _    ║
    ║    /\___/\       __   __     __   |    __  _ __       __   __    |    ║
    ║   |       |     /  \ /  \   /  \ -+-  |  \ |/  \     /  \ /  \  -+-   ║
    ║  _  *   *  _   |      __ |  \__   |   +--/ |        |      __ |  |    ║
    ║  -   /_\   -   |     /  \|     \  |   |    |        |     /  \|  |    ║
    ║      ---        \__/_\__/\__\__/___\_/ \__/|_        \__/_\__/\___\_  ║
    ║                                                                       ║
    ║ Hello Kitty!              gives your MONOTYPE nine lives              ║
    ║                                                                       ║
    ╚═══════════════════════════════════════════════════════════════════════╝
    """
    try:
        ui.display('\nOh, this was meowsome.\n')
        ui.display(text)
    except (OSError, ImportError, FileNotFoundError):
        print('There are no Easter Eggs in this program.')
