/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package org.smartbricks.i2c;

import com.pi4j.io.i2c.I2CBus;
import com.pi4j.io.i2c.I2CDevice;
import com.pi4j.io.i2c.I2CFactory;
import java.io.IOException;
import java.util.Arrays;
import org.smartbricks.core.Engine;
import org.smartbricks.core.Log;
import org.smartbricks.exceptions.BadArgumentException;
import org.smartbricks.exceptions.NoSuchEventException;
import org.smartbricks.peripherals.Event;
import org.smartbricks.peripherals.Peripheral;
import org.smartbricks.peripherals.PeripheralState;

/**
 * Provides a wrapper for all I2C-Bus features
 * @author cambierr
 */
public class Bus {

    private static final String TAG = Bus.class.getSimpleName();
    public static final byte ADDRESS_FROM = 0x03;
    public static final byte ADDRESS_TO = 0x77;
    public static final Boolean synchronizer = true;

    private static I2CBus bus;

    /**
     * Returns a string representation of this Bus object
     *
     * @return a string representation of this Bus object
     */
    @Override
    public String toString() {
        return "Bus {" + bus + "}";
    }

    /**
     * Init the Bus using this instance as bus master
     *
     * @param _busAddress the address the bus is located to
     * @see I2CBus constants
     * @throws IOException if bus address is wrong or if bus is in fault
     */
    public static void init(int _busAddress) throws IOException {
        bus = I2CFactory.getInstance(_busAddress);
    }

    /**
     * Listen only once for the bus, trying to make a full discovery of attached
     * peripherals
     */
    public static void listenOnce() {
        listenOnce(false);
    }

    /**
     * Listen only once for the bus, trying to make a full discovery of attached
     * peripherals if _activeOnly is set to true
     *
     * @param _activeOnly true for discovering new peripherals, false for
     * listenning to registered one only
     */
    public static void listenOnce(boolean _activeOnly) {

        Log.d(TAG, "Starting BUS listenning");
        for (byte i = Bus.ADDRESS_FROM; i <= Bus.ADDRESS_TO; i++) {
            Peripheral p = Engine.getPeripheralAt(i);
            if (_activeOnly && p == null) {
                continue;
            }

            synchronized (synchronizer) {
                try {
                    I2CDevice device = bus.getDevice(i);

                    byte[] message = new byte[66];

                    device.read(message, 0, 66);

                    if (p != null && p.getState() == PeripheralState.MISSING) {
                        p.setState(PeripheralState.OK);
                        p.getEvents().stream().filter((ev) -> ("join".equals(ev.getName()))).forEach((ev) -> {
                            try {
                                p.dispatchEvent(ev, null);
                            } catch (NoSuchEventException ex) {
                                Log.s(TAG, "Could not dispatch error event on peripheral", ex);
                            }
                        });
                    }

                    switch (message[0]) {
                        case MessageHeader.JOIN_INIT:
                            Log.i(TAG, "New device joining at address: " + i);
                            Client.handleJoin(bus, i, device, message);
                            break;
                        case MessageHeader.NOTHING:
                            Log.d(TAG, "Device {" + i + "} has nothing for master");
                            break;
                        case MessageHeader.ERROR:
                            if (p != null) {
                                p.setState(PeripheralState.ERROR);
                                p.getEvents().stream().filter((ev) -> ("error".equals(ev.getName()))).forEach((ev) -> {
                                    try {
                                        p.dispatchEvent(ev, null);
                                    } catch (NoSuchEventException ex) {
                                        Log.s(TAG, "Could not dispatch error event on peripheral", ex);
                                    }
                                });
                                Engine.dispatchEvent(org.smartbricks.core.Event.ERROR, p);
                            }
                            break;
                        case MessageHeader.EVENT:

                            byte[] events = Arrays.copyOfRange(message, 2, message[1] + 2);

                            if (p == null) {
                                Log.w(TAG, "Event {" + byteArrayToString(events) + "} received from device at {" + i + "} with no peripheral attached");
                                break;
                            }

                            byte event = -1;
                            byte[] data = new byte[2];

                            for (int j = 0; j < events.length; j++) {

                                for (int k = 0; k < 3; k++) {
                                    if (k == 0) {
                                        event = events[j];
                                        j++;
                                        continue;
                                    }
                                    data[k - 1] = events[j];
                                    if (k == 2) {
                                        break;
                                    }
                                    j++;
                                }
                                for (Event e : p.getEvents()) {
                                    if (e.getIdentifier() == ((event < 0) ? 256 + event : event)) {
                                        try {
                                            p.dispatchEvent(e, e.interpretData(data));
                                        } catch (NoSuchEventException | BadArgumentException ex) {
                                            Log.e(TAG, "Could not dispatch event {" + ((event < 0) ? 256 + event : event) + "} to peripheral {" + Client.idToString(p.getId()) + "}", ex);
                                        }
                                        break;
                                    }
                                }
                            }

                            break;
                        default:
                            Log.w(TAG, "Unkwown message header received: " + message[0]);
                    }

                } catch (IOException ex) {
                    if (p != null) {
                        Log.e(TAG, "error while joining device {" + i + "}", ex);
                        p.setState(PeripheralState.MISSING);
                        p.getEvents().stream().filter((ev) -> ("quit".equals(ev.getName()))).forEach((ev) -> {
                            try {
                                p.dispatchEvent(ev, null);
                            } catch (NoSuchEventException ex1) {
                                Log.s(TAG, "Could not dispatch quit event on peripheral", ex1);
                            }
                        });
                        Engine.dispatchEvent(org.smartbricks.core.Event.QUIT, p);
                    } else {
                        Log.d(TAG, "No device found at address {" + i + "}", ex);
                    }
                }
            }
        }

    }
    /**
     * Converts a byte-array to a string representation
     * @param _array the byte-array
     * @return the string representation
     */
    private static String byteArrayToString(byte[] _array) {
        String out = "";
        for (int i = 0; i < _array.length; i++) {
            if (i == _array.length - 1) {
                out += _array[i];
            } else {
                out += _array[i] + " ";
            }
        }
        return out;
    }

}
