// Written by Jürgen Moßgraber - mossgrabers.de
// (c) 2017-2020
// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt

package de.mossgrabers.controller.fire.controller;

import de.mossgrabers.framework.controller.display.AbstractGraphicDisplay;
import de.mossgrabers.framework.daw.IHost;
import de.mossgrabers.framework.daw.midi.IMidiOutput;
import de.mossgrabers.framework.graphics.ChromaticGraphicsConfiguration;
import de.mossgrabers.framework.graphics.DefaultGraphicsDimensions;
import de.mossgrabers.framework.graphics.IBitmap;

import java.util.Arrays;


/**
 * The display of the Akai Fire.
 *
 * @author Jürgen Moßgraber
 */
public class FireDisplay extends AbstractGraphicDisplay
{
    // @formatter:off
    private static final int [][] BIT_MUTATE =
    {
        { 13,  19,  25,  31,  37,  43,  49 },
        {  0,  20,  26,  32,  38,  44,  50 },
        {  1,   7,  27,  33,  39,  45,  51 },
        {  2,   8,  14,  34,  40,  46,  52 },
        {  3,   9,  15,  21,  41,  47,  53 },
        {  4,  10,  16,  22,  28,  48,  54 },
        {  5,  11,  17,  23,  29,  35,  55 },
        {  6,  12,  18,  24,  30,  36,  42 }
      };
    // @formatter:on

    private static final int       STRIPE_SIZE   = 147;
    private static final int       PACKET_SIZE   = 4 + STRIPE_SIZE;

    private final IMidiOutput      output;
    private final int [] []        oledBitmap    = new int [8] [STRIPE_SIZE];
    private final int [] []        oldOledBitmap = new int [8] [STRIPE_SIZE];
    private final byte []          data          = new byte [12 + STRIPE_SIZE];

    private long                   lastSend      = System.currentTimeMillis ();


    /**
     * Constructor. The display is divided into eight bands of 8×128 pixels. Each band in this
     * arrangement is written in a block of 8×7 pixels
     *
     * @param host The host
     * @param output The midi output which addresses the display
     * @param maxParameterValue The maximum parameter value (upper bound)
     */
    public FireDisplay (final IHost host, final IMidiOutput output, final int maxParameterValue)
    {
        super (host, new ChromaticGraphicsConfiguration (), new DefaultGraphicsDimensions (128, 64, maxParameterValue), "Fire Display");

        this.output = output;

        this.data[0] = (byte) 0xF0;
        this.data[1] = 0x47; // AKAI
        this.data[2] = 0x7F; // All-Call
        this.data[3] = 0x43; // Fire
        this.data[4] = 0x0E; // WRITE OLED

        // Payload length high
        this.data[5] = (byte) (PACKET_SIZE / 128);
        // Payload length low
        this.data[6] = (byte) (PACKET_SIZE % 128);

        // Start colum of update
        this.data[9] = 0x00;
        // End column of update
        this.data[10] = 0x7f;

        this.data[this.data.length - 1] = (byte) 0xF7;
    }


    /** {@inheritDoc} */
    @Override
    public void notify (final String message)
    {
        if (message == null)
            return;
        this.host.showNotification (message);
        this.setNotificationMessage (message);
    }


    /** {@inheritDoc} */
    @Override
    protected void send (final IBitmap image)
    {
        synchronized (this.data)
        {
            image.encode ( (imageBuffer, width, height) -> {

                // Unwind 128x64 arrangement into a 1024x8 arrangement of pixels
                for (int stripe = 0; stripe < 8; stripe++)
                {
                    for (int y = 0; y < height / 8; y++)
                    {
                        for (int x = 0; x < width; x++)
                        {
                            final int blue = imageBuffer.get ();
                            final int green = imageBuffer.get ();
                            final int red = imageBuffer.get ();
                            imageBuffer.get (); // Drop unused Alpha

                            final int xpos = x + 128 * (y / 8);
                            final int ypos = y % 8;

                            // Remap by tiling 7x8 block of translated pixels
                            final int remapBit = BIT_MUTATE[ypos][xpos % 7];
                            final int idx = xpos / 7 * 8 + remapBit / 7;
                            if (blue + green + red < 0)
                                this.oledBitmap[stripe][idx] |= 1 << remapBit % 7;
                            else
                                this.oledBitmap[stripe][idx] &= ~(1 << remapBit % 7);
                        }
                    }
                }
            });

            // Convert to sysex and send to device
            for (int stripe = 0; stripe < 8; stripe++)
            {
                // Start 8-pixel band of update
                this.data[7] = (byte) stripe;
                // End 8-pixel band of update (here, 8 bands of 8 pixels, i.e. the whole display)
                this.data[8] = (byte) stripe;

                for (int i = 0; i < STRIPE_SIZE; i++)
                    this.data[11 + i] = (byte) this.oledBitmap[stripe][i];

                // Slow down display updates to not flood the device controller
                // Send if content has change or every 3 seconds if there was no change to keep
                // the display from going into sleep mode
                final long now = System.currentTimeMillis ();
                if (Arrays.compare (this.oledBitmap[stripe], this.oldOledBitmap[stripe]) == 0 && now - this.lastSend < 3000)
                    continue;
                System.arraycopy (this.oledBitmap[stripe], 0, this.oldOledBitmap[stripe], 0, STRIPE_SIZE);
                this.lastSend = now;

                this.output.sendSysex (this.data);
            }
        }
    }
}