/*
 * Copyright (C) 2020-2021 Aloysius Indrayanto
 *
 * This file is part of the eCxx library, see LICENSE file for the license details.
 */


import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

import javax.swing.*;


public class LEDSimulator {

    // Constants
    private static final int MINIMUM_LED_SIZE = 1;
    private static final int MAXIMUM_LED_SIZE = 5;

    private static final int DEFAULT_LED_SIZE = 3;

    // Supported memory layouts
    private enum SDFMemoryLayout {
           RowMajor,    RowMajorRX,    RowMajorRY,    RowMajorRXY,
        AltRowMajor, AltRowMajorRX, AltRowMajorRY, AltRowMajorRXY
        ;

        public static boolean revX(SDFMemoryLayout ml)
        {
            return (ml ==    RowMajorRX) || (ml ==    RowMajorRXY) ||
                   (ml == AltRowMajorRX) || (ml == AltRowMajorRXY);
        }

        public static boolean revY(SDFMemoryLayout ml)
        {
            return (ml ==    RowMajorRY) || (ml ==    RowMajorRXY) ||
                   (ml == AltRowMajorRY) || (ml == AltRowMajorRXY);
        }
    }

    // Supported color types
    private enum SDFColorType {
         HSV  ,  HSL  ,
            W ,
         RGB  ,  RBG  ,  GRB  ,  GBR  ,  BRG  ,  BGR  ,
         RGBW ,  RBGW ,  GRBW ,  GBRW ,  BRGW ,  BGRW ,
        WRGB  , WRBG  , WGRB  , WGBR  , WBRG  , WBGR  ,
         RGBX ,  RBGX ,  GRBX ,  GBRX ,  BRGX ,  BGRX ,
        XRGB  , XRBG  , XGRB  , XGBR  , XBRG  , XBGR
    };

    // Parsing-related variables
    private static int sdfLineNumber = 0;

    // Simulation-related variables
    private static int             xSize     = 0;     // The number of X pixels (columns)
    private static int             ySize     = 0;     // The number of Y pixels (rows)
    private static int             lSize     = 0;     // LED size
    private static int             nSize     = 0;     // Padding size
    private static int             delayMS   = 0;     // Frame delay
    private static boolean         loopFrame = false; // Loop flag
    private static int             exitDelay = -1;    // Exit delay
    private static int             exitCnt   = 0;     // Exit counter

    private static int             cWidth    = 0;     // Preferred canvas width
    private static int             cHeight   = 0;     // Preferred canvas height

    private static List<Color[][]> colorData = new ArrayList<Color[][]>();
    private static int             frameCnt  = 0;

    // Canvas class
    private static class Canvas extends JPanel implements ActionListener {
        public void actionPerformed(ActionEvent arg0)
        {
            repaint();
            getToolkit().sync();
        }

        protected void paintComponent(Graphics g)
        {
            // Get the frame data and update the frame counter
            if( frameCnt >= colorData.size() ) {
                if(loopFrame) {
                    // Loop back to the first frame
                    frameCnt = 0;
                }
                else {
                    // Stay at the last frame
                    frameCnt = colorData.size() - 1;
                    // Process exit delay as needed
                    if(exitDelay >= 0) {
                        ++exitCnt;
                        if(exitCnt > exitDelay) System.exit(0);
                    }
                }
            }

            final Color[][] curFrame = colorData.get(frameCnt);
            ++frameCnt;

            // Calculate the needed paddings to center the drawing
            final Dimension size = getSize();

            int padX = 0;
            int padY = 0;

            if(size.width  > cWidth ) padX = (size.width  - cWidth ) / 2;
            if(size.height > cHeight) padY = (size.height - cHeight) / 2;

            // Calculate the LEDs' dimension
            final int ledOverallSize     = lSize - nSize * 1;
            final int ledBodyOffset      = nSize * 2;
            final int ledBodyRound       = nSize / 2;
            final int ledBorderOffset    = nSize * 3;
            final int ledFaceOffset      = ledBorderOffset + 1;
            final int ledFaceLightOffset = ledBorderOffset;

            // Prepare two images for drawing
            BufferedImage biBody  = new BufferedImage(size.width, size.height, BufferedImage.TYPE_3BYTE_BGR);
            BufferedImage biLight = new BufferedImage(size.width, size.height, BufferedImage.TYPE_3BYTE_BGR);

            Graphics2D    g2Body  = (Graphics2D) biBody .getGraphics();
            Graphics2D    g2Light = (Graphics2D) biLight.getGraphics();

            g2Body .setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2Light.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            // Draw the bodies and lights of all the LEDs into two separated images
            for(int y = 0; y < ySize; ++y) {
                for(int x = 0; x < xSize; ++x) {
                    // Get the coordinates and color for the current LED
                    final int   xl = padX + x * lSize + nSize * 2;
                    final int   yl = padY + y * lSize + nSize * 2;
                    final Color cl = curFrame[y][x];
                    // Determine the colors for the LED's body
                    final Color colorPinOuter = new Color( 64,  64,  64);
                    final Color colorPinInner = new Color( 96,  96,  96);
                    final Color colorBody     = new Color(128, 128, 128);
                    final Color colorBorder   = new Color( 96,  96,  96);
                    final Color colorFace     = new Color(  0,   0,   0);
                    // Calculate coordinates for the LED's body
                    final int bodyX = xl + ledBodyOffset;
                    final int bodyY = yl + ledBodyOffset;
                    final int bodyW = ledOverallSize - ledBodyOffset * 2;
                    final int bodyH = ledOverallSize - ledBodyOffset * 2;
                    final int pinWa = lSize / 10;
                    final int pinHa = (bodyH + 5) / 10;
                    final int pinW  = bodyW + pinWa * 2;
                    final int pinH  = bodyH / 5;
                    final int pinX  = bodyX - pinWa;
                    final int pinY1 = bodyY +                pinHa;
                    final int pinY2 = bodyY + bodyH - pinH - pinHa;
                    // Draw the LED's body - pins (outer)
                    g2Body.setColor(colorPinOuter);
                    g2Body.fillRect(pinX - 1, pinY1, pinW + 2, pinH);
                    g2Body.fillRect(pinX - 1, pinY2, pinW + 2, pinH);
                    // Draw the LED's body - pins (inner)
                    g2Body.setColor(colorPinInner);
                    g2Body.fillRect(pinX, pinY1, pinW, pinH);
                    g2Body.fillRect(pinX, pinY2, pinW, pinH);
                    // Draw the LED's body - outer body
                    g2Body.fillRoundRect(bodyX, bodyY, bodyW, bodyH, ledBodyRound, ledBodyRound);
                    // Draw the LED's body - inner body
                    g2Body.setColor     (colorBody);
                    g2Body.fillRoundRect(bodyX + 1, bodyY + 1, bodyW - 2, bodyH - 2, ledBodyRound, ledBodyRound);
                    // Draw the LED's body - face border
                    g2Body.setColor(colorBorder);
                    g2Body.fillOval(xl + ledBorderOffset, yl + ledBorderOffset, ledOverallSize - ledBorderOffset * 2, ledOverallSize - ledBorderOffset * 2);
                    // Draw the LED's body - face
                    g2Body.setColor(colorFace);
                    g2Body.fillOval(xl + ledFaceOffset, yl + ledFaceOffset, ledOverallSize - ledFaceOffset * 2, ledOverallSize - ledFaceOffset * 2);
                    // Draw the LED's light
                    if(cl != null) {
                        // Determine the colors for the LED's light
                        final Color dark1 = cl   .darker();
                        final Color dark2 = dark1.darker();
                        final Color dark3 = dark2.darker();
                        final Color dark4 = dark3.darker();
                        // Draw the halo light
                        g2Light.setColor(dark4); g2Light.fillOval(xl + 0, yl + 0, ledOverallSize - 0 * 2, ledOverallSize - 0 * 2);
                        g2Light.setColor(dark3); g2Light.fillOval(xl + 2, yl + 2, ledOverallSize - 2 * 2, ledOverallSize - 2 * 2);
                        g2Light.setColor(dark2); g2Light.fillOval(xl + 4, yl + 4, ledOverallSize - 4 * 2, ledOverallSize - 4 * 2);
                        g2Light.setColor(dark1); g2Light.fillOval(xl + 6, yl + 6, ledOverallSize - 6 * 2, ledOverallSize - 6 * 2);
                        // Draw the face light
                        g2Light.setColor(cl);
                        g2Light.fillOval(xl + ledFaceLightOffset, yl + ledFaceLightOffset, ledOverallSize - ledFaceLightOffset * 2, ledOverallSize - ledFaceLightOffset * 2);
                    }
                } // for x
            } // for y

            // Blend the two images
            for(int y = 0; y < size.height; ++y) {
                for(int x = 0; x < size.width; ++x) {
                    // Get and extract the colors
                    final int rgb1 = biBody .getRGB(x, y);
                    final int rgb2 = biLight.getRGB(x, y);
                    final int r1   = ( (rgb1 >> 16) & 0x000000FF );
                    final int g1   = ( (rgb1 >>  8) & 0x000000FF );
                    final int b1   = (  rgb1        & 0x000000FF );
                    final int r2   = ( (rgb2 >> 16) & 0x000000FF );
                    final int g2   = ( (rgb2 >>  8) & 0x000000FF );
                    final int b2   = (  rgb2        & 0x000000FF );
                    // Mix the colors
                    final int rMix = r1 + r2;
                    final int gMix = g1 + g2;
                    final int bMix = b1 + b2;
                    // Clip the resulting color
                    final int rClp = (rMix >= 255) ? 255 : rMix;
                    final int gClp = (gMix >= 255) ? 255 : gMix;
                    final int bClp = (bMix >= 255) ? 255 : bMix;
                    // Draw back the resulting color
                    biBody.setRGB( x, y, (rClp << 16) | (gClp << 8) | bClp );
                } // for x
            } // for y

            // Draw the image
            super.paintComponent(g);
            g.drawImage(biBody, 0, 0, null);
        }
    }

    // GUI-related variables
    private static JFrame mainWindow = null;
    private static Canvas canvas     = null;

    // Print error and exit
    private static void printErrorAndExit(String msg)
    {
        System.out.print  ("\nLEDSimulator: *** [#");
        System.out.print  (Integer.toString(sdfLineNumber) + "] ERROR \"" + msg);
        System.out.println("\"\n");

        System.exit(-1);
    }

    // Parse integer with error checking
    private static int parseInt(String text)
    {
        try {
            return Integer.parseInt(text);
        }
        catch(NumberFormatException e) {
            printErrorAndExit(
                "Invalid integer value!"
            );
        }

        return 0;
    }

    // Simulate the non-linear response of LED
    // Based on https://jared.geek.nz/2013/feb/linear-led-pwm
    private static float cie1931Lightness(int v)
    {
        final double y = (double) v / 255.0;

        double l;
        if(y <= 0.008856)
            l = 903.3 * y;
        else
            l = 119.0 * Math.pow(y, 1.0 / 3.0) - 16.0;

        l /= 100.0;

        if(l < 0.0) return 0.0f;
        if(l > 1.0) return 1.0f;

        return (float) l;
    }

    // Create color from the specified HSV values
    private static Color fromHSV(int h, int s, int v)
    {
        // Based on https://en.wikipedia.org/wiki/HSL_and_HSV

        // Determine the sextant
        final int sextant = h / 60; // 0 ... 6

        // Determine the fractional value
        final int fract = h - sextant * 60; // 0 ... 60

        // Determine the lower value
        final int l = v * (200 - s) / 200; // 0 ... 200

        // Calculate the interpolated and reverse-interpolated values
        final int i = (v - l) * (     fract) / 60 + l;
        final int n = (v - l) * (60 - fract) / 60 + l;

        // Determine the RGB color
        int r = 0;
        int g = 0;
        int b = 0;

        switch( (sextant < 6) ? sextant : 0 ) {
            case 0 : r = v; g = i; b = l; break;
            case 1 : r = n; g = v; b = l; break;
            case 2 : r = l; g = v; b = i; break;
            case 3 : r = l; g = n; b = v; break;
            case 4 : r = i; g = l; b = v; break;
            case 5 : r = v; g = l; b = n; break;
        }

        // Scale and return the color
        return new Color(
            cie1931Lightness(r * 255 / 200),
            cie1931Lightness(g * 255 / 200),
            cie1931Lightness(b * 255 / 200)
        );
    }

    // Create color from the specified HSL values
    private static Color fromHSL(int h, int s, int l)
    {
        // Based on https://en.wikipedia.org/wiki/HSL_and_HSV

        final int vh = h;
        final int vv = l + s * Math.min(l, 200 - l) / 200;
        final int vs = (vv != 0) ? (400 - l * 400 / vv) : 0;

        // Create from HSV values
        return fromHSV(vh, vs, vv);
    }

    // Check for premature end of file
    private static void sdfCheckForPrematureEOF(String fileName, String line)
    {
        if(line != null) return;

        printErrorAndExit("Premature end of file in simulation data file!");
    }

    // Get (read) a line from the file, discarding empty line(s) and comment(s)
    private static String sdfReadLine(BufferedReader br)
    {
        String line;

        while(true) {
            // Try read line
            try {
                // Read line
                line = br.readLine();
                // Increment the line number
                ++sdfLineNumber;
                // Check if there is no more line to read
                if(line == null) return null;
            }
            catch(Exception e) {
                // Error, assume there is no more line to read
                return null;
            }
            // Trim all whitespaces
            line = line.trim();
            // Check if the line is not empty and not a comment line
            if( !line.isEmpty() && line.charAt(0) != '#' ) {
                // Remove possible comment at the end of the line and trim all whitespaces
                line = line.split("#")[0].trim();
                // Return the line if it is not empty
                if( !line.isEmpty() ) return line;
            }
        }
    }

    // Parse color data
    private static Color sdfParseColorData(String colorType, String lc)
    {
        // Parse and check the line
        final String[] cc = lc.split(",");
        if( cc.length != colorType.length() ) printErrorAndExit(
            "Invalid color data definition for \"" + colorType + "\" color type!"
        );

        // Parse and check the color data
        int h = 0;
        int s = 0;
        int v = 0;
        int l = 0;
        int r = 0;
        int g = 0;
        int b = 0;
        int w = 0;

        for(int i = 0; i < colorType.length(); ++i) {
            // Convert to integer and check
            int n = parseInt( cc[i].trim() );
            if(n < 0) printErrorAndExit(
                "Invalid color data value for \"" + colorType + "\" color type!"
            );
            // Store to the appropriate variable
            switch( colorType.charAt(i) ) {
                case 'H' : h = n; break;
                case 'S' : s = n; break;
                case 'V' : v = n; break;
                case 'L' : l = n; break;
                case 'R' : r = n; break;
                case 'G' : g = n; break;
                case 'B' : b = n; break;
                case 'W' : w = n; break;
            }
        }

        r += w;
        g += w;
        b += w;

        if( colorType.equals("HSV") || colorType.equals("HSL") ) {
            if(h > 360 || s > 200 || v > 200 || l > 200) printErrorAndExit(
                "Invalid color data value for \"" + colorType + "\" color type!"
            );
        }
        else {
            if(r > 255 || g > 255 || b > 255) printErrorAndExit(
                "Invalid color data value for \"" + colorType + "\" color type!"
            );
        }

        // Perform adjustment on the color data and return it as a color object
        if( colorType.equals("HSV") ) {
            // Return null for a full black color
            if(v == 0)
                return null;
            // Otherwise, return a color object
            else
                return fromHSV(h, s, v);
        }
        else if( colorType.equals("HSL") ) {
            // Return null for a full black color
            if(l == 0)
                return null;
            // Otherwise, return a color object
            else
                return fromHSL(h, s, l);
        }
        else {
            // Return null for a full black color
            if(r == 0 && g == 0 && b == 0)
                return null;
            // Otherwise, return a color object
            else
                return new Color( cie1931Lightness(r), cie1931Lightness(g), cie1931Lightness(b) );
        }
    }

    // Parse the simulation data file
    private static void sdfParse(String fileName, BufferedReader br)
    {
        // Print the simulation data file name
        System.out.println("\nUsing simulation data file \"" + fileName + "\" ...");

        // Get and check the memory layout
        final String memoryLayoutStr = sdfReadLine(br);
        sdfCheckForPrematureEOF(fileName, memoryLayoutStr);

        SDFMemoryLayout memoryLayout = null;
        try {
            memoryLayout = SDFMemoryLayout.valueOf(memoryLayoutStr);
        }
        catch(Exception e) {
            printErrorAndExit("Unsupported memory layout!");
        }

        // Get and check the color type
        final String colorType = sdfReadLine(br);
        sdfCheckForPrematureEOF(fileName, colorType);

        try {
            SDFColorType.valueOf(colorType);
        }
        catch(Exception e) {
            printErrorAndExit("Unsupported color type!");
        }

        // Get and check the X size
        final String strXSize = sdfReadLine(br);
        sdfCheckForPrematureEOF(fileName, strXSize);

        xSize = parseInt(strXSize);
        if(xSize <= 0) printErrorAndExit("Invalid X size!");

        // Get and check the Y size
        final String strYSize = sdfReadLine(br);
        sdfCheckForPrematureEOF(fileName, strYSize);

        ySize = parseInt(strYSize);
        if(ySize <= 0) printErrorAndExit("Invalid Y size!");

        // Get and check the LED size
        final String strLSize = sdfReadLine(br);
        sdfCheckForPrematureEOF(fileName, strLSize);

        lSize = parseInt(strLSize);

        if(lSize == 0) lSize = DEFAULT_LED_SIZE;
        if(lSize < MINIMUM_LED_SIZE || lSize > MAXIMUM_LED_SIZE) printErrorAndExit("Invalid LED size!");

        lSize = lSize * 10;
        nSize = lSize / 10;

        lSize = lSize + nSize * 2;

        // Get and check the delay multiplier
        final String frameDelay = sdfReadLine(br);
        sdfCheckForPrematureEOF(fileName, frameDelay);

        delayMS = parseInt(frameDelay);
        if( delayMS <= 0 ) printErrorAndExit("Invalid frame delay!");

        delayMS = delayMS * 10;

        // Get and check the loop mode
        final String loopMode = sdfReadLine(br);
        sdfCheckForPrematureEOF(fileName, loopMode);

        if( loopMode.equals("loop") ) {
            loopFrame = true;
        }
        else { // noloop
            // Parse and check the mode and the exit delay value
            final String[] lmc = loopMode.split(" ");
            if( !lmc[0].equals("noloop") || lmc.length > 2 ) printErrorAndExit("Invalid loop mode!");
            if(lmc.length == 2) {
                exitDelay = parseInt( lmc[1].trim() );
                if(exitDelay < 0) printErrorAndExit(
                    "Invalid exit delay value for \"noloop\" loop mode!"
                );
            }
        }

        // Parse the color data
        while(true) {
            for(int y = 0; y < ySize; ++y) {
                for(int x = 0; x < xSize; ++x) {
                    // Read line
                    final String lc = sdfReadLine(br);
                    if(y == 0 && x == 0) {
                        // Check if all frames have been parsed
                        if(lc == null) {
                            // Done parsing
                            System.out.println("");
                            return;
                        }
                        // Allocate memory for the new frame
                        colorData.add( new Color[ySize][xSize] );
                    }
                    else {
                        sdfCheckForPrematureEOF(fileName, lc);
                    }
                    // Get the current frame buffer
                    final Color[][] curFrame = colorData.get( colorData.size() - 1 );
                    // Reverse the coordinate(s) as needed
                    final int fx = SDFMemoryLayout.revX(memoryLayout) ? (xSize - 1 - x) : x;
                    final int fy = SDFMemoryLayout.revY(memoryLayout) ? (ySize - 1 - y) : y;
                    // Parse the color data
                    final Color color = sdfParseColorData(colorType, lc);
                    // Store the color data based on the memory layout
                    switch(memoryLayout) {
                        // Plain
                        case RowMajor    :
                        case RowMajorRX  :
                        case RowMajorRY  :
                        case RowMajorRXY :
                             curFrame[fy][fx] = color;
                             break;
                        // Plain with alternating pixel direction
                        case AltRowMajor    :
                        case AltRowMajorRX  :
                        case AltRowMajorRY  :
                        case AltRowMajorRXY :
                            if( (y & 1) == 1 ) curFrame[fy][xSize - 1 - fx] = color;
                            else               curFrame[fy][            fx] = color;
                            break;
                    } // switch memoryLayout
                } // for x
            } // for y
        } // while true
    }

    // Java main()
    public static void main(String args[]) throws Exception
    {
        // Check the number of arguments
        if(args.length != 1) {
            System.out.println("\n");
            System.out.println("Usage: LED Simulator <simulation_data_file>");
            System.out.println("\n");
            return;
        }

        // Open and parse the simulation data file
        final String         fn  = args[0];
        final BufferedReader fbr = new BufferedReader( new FileReader( new File(fn) ) );
        sdfParse(args[0], fbr);
        fbr.close();

        // Create the main window
        mainWindow = new JFrame("LED Simulator (" + fn + ")");

        mainWindow.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        mainWindow.getRootPane().setBorder( BorderFactory.createEmptyBorder(10, 10, 10, 10) );

        // Create the canvas
        cWidth  = xSize * lSize + nSize * 2 + 2;
        cHeight = ySize * lSize + nSize * 2 + 2;

        canvas = new Canvas();
        canvas.setDoubleBuffered(true);

        canvas.setBackground   ( Color.BLACK );
        canvas.setMinimumSize  ( new Dimension(cWidth, cHeight) );
        canvas.setPreferredSize( new Dimension(cWidth, cHeight) );

        // Create the scroll pane and add the canvas to the main window
        final JScrollPane scrollPane = new JScrollPane(
            canvas,
            JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
            JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS
        );

        scrollPane.getViewport().setMinimumSize  ( new Dimension(cWidth, cHeight) );
        scrollPane.getViewport().setPreferredSize( new Dimension(cWidth, cHeight) );

        mainWindow.add(scrollPane);

        // Add event listeners to the main window
        mainWindow.addWindowFocusListener( new WindowFocusListener() {
            private long lastTime = 0;
            public void windowGainedFocus(WindowEvent e) {
                lastTime = System.currentTimeMillis();
            }
            public void windowLostFocus(WindowEvent e) {
                final long newTime = System.currentTimeMillis();
                if(newTime - lastTime > 250) {
                    mainWindow.setAlwaysOnTop(false);
                    mainWindow.removeWindowFocusListener(this);
                }
                lastTime = newTime;
            }
        } );

        mainWindow.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
            KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "Exit"
        );

        mainWindow.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
            KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Exit"
        );

        mainWindow.getRootPane().getActionMap().put(
            "Exit",
            new AbstractAction() {
                public void actionPerformed(ActionEvent e)
                { System.exit(0); }
            }
        );

        // Make the window to stay above all other windows
        mainWindow.setAlwaysOnTop(true);

        // Resize and show the window
        mainWindow.pack();

        final Insets iw = mainWindow.getInsets();
        final Insets is = scrollPane.getInsets();
        final int    aw = iw.left + iw.right  + is.left + is.right  + scrollPane.getVerticalScrollBar  ().getSize().width;
        final int    ah = iw.top  + iw.bottom + is.top  + is.bottom + scrollPane.getHorizontalScrollBar().getSize().height;

        int ww = cWidth  + aw;
        int wh = cHeight + ah;

        if(ww > 600) ww = 600;
        if(wh > 425) wh = 425;

        mainWindow.setMinimumSize  ( new Dimension(ww, wh) );
        mainWindow.setPreferredSize( new Dimension(ww, wh) );

        mainWindow.setVisible(true);

        // Create and start the animation timer
        ( new Timer(delayMS, canvas) ).start();

        // Pressing enter on the console will exit the application
        ( new Thread() {
            public void run() {
                for(;;) {
                    yield();
                    try {
                        final int c = System.in.read();
                        if(c == 0x0A || c == 0x0D || c == 0x1B) System.exit(0);
                    }
                    catch(Exception e) {}
                }
            }
        } ).start();
    }

} // class
