/*  Copyright (C) 2016-2024 Andreas Shimokawa, Daniele Gobbetti

    This file is part of Gadgetbridge.

    Gadgetbridge is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Gadgetbridge 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.ble;

import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;

public class PebbleLESupport {
    private static final Logger LOG = LoggerFactory.getLogger(PebbleLESupport.class);
    private final GBDevice mgbDevice;
    private final BluetoothDevice mBtDevice;
    private PipeReader mPipeReader;
    private PebbleGATTServer mPebbleGATTServer;
    private PebbleGATTClient mPebbleGATTClient;
    private PipedInputStream mPipedInputStream;
    private PipedOutputStream mPipedOutputStream;
    private int mMTU = 20;
    private int mMTULimit = Integer.MAX_VALUE;
    public boolean clientOnly = false; // currently experimental, and only possible for Pebble 2
    private boolean mIsConnected = false;
    private HandlerThread mWriteHandlerThread;
    private Handler mWriteHandler;

    public PebbleLESupport(Context context, GBDevice gbDevice, final BluetoothDevice btDevice, PipedInputStream pipedInputStream, PipedOutputStream pipedOutputStream) throws IOException {
        mgbDevice = gbDevice;
        mBtDevice = btDevice;
        mPipedInputStream = new PipedInputStream();
        mPipedOutputStream = new PipedOutputStream();
        try {
            pipedOutputStream.connect(mPipedInputStream);
            pipedInputStream.connect(mPipedOutputStream);
        } catch (IOException e) {
            LOG.warn("could not connect input stream");
        }

        mWriteHandlerThread = new HandlerThread("write handler thread");
        mWriteHandlerThread.start();
        mWriteHandler = new Handler(mWriteHandlerThread.getLooper());

        mMTULimit = GBApplication.getDevicePrefs(mgbDevice).getInt("pebble_mtu_limit", 512);
        mMTULimit = Math.max(mMTULimit, 20);
        mMTULimit = Math.min(mMTULimit, 512);

        clientOnly = GBApplication.getDevicePrefs(mgbDevice).getBoolean("pebble_gatt_clientonly", false);

        if (!clientOnly) {
            mPebbleGATTServer = new PebbleGATTServer(this, context, mBtDevice);
        }
        if (clientOnly || mPebbleGATTServer.initialize()) {
            mPebbleGATTClient = new PebbleGATTClient(this, context, mBtDevice);
            try {
                synchronized (this) {
                    wait(30000);
                    if (mIsConnected) {
                        return;
                    }
                }
            } catch (InterruptedException ignored) {
            }
        }
        this.close();
        throw new IOException("connection failed");
    }

    private void writeToPipedOutputStream(byte[] value, int offset, int count) {
        try {
            mPipedOutputStream.write(value, offset, count);
        } catch (IOException e) {
            LOG.warn("error writing to output stream", e);
        }
    }

    synchronized public void close() {
        destroyPipedInputReader();
        if (mPebbleGATTServer != null) {
            mPebbleGATTServer.close();
            mPebbleGATTServer = null;
        }
        if (mPebbleGATTClient != null) {
            mPebbleGATTClient.close();
            mPebbleGATTClient = null;
        }
        try {
            mPipedInputStream.close();
        } catch (IOException ignore) {
        }
        try {
            mPipedOutputStream.close();
        } catch (IOException ignore) {
        }
        if (mWriteHandlerThread != null) {
            mWriteHandlerThread.quit();
        }
    }

    private synchronized void createPipedInputReader() {
        if (mPipeReader == null) {
            mPipeReader = new PipeReader();
        }
        if (!mPipeReader.isAlive()) {
            mPipeReader.start();
        }
    }

    synchronized private void destroyPipedInputReader() {
        if (mPipeReader != null) {
            mPipeReader.interrupt();
            try {
                mPipeReader.join();
            } catch (InterruptedException e) {
                LOG.error(e.getMessage());
            }
            mPipeReader = null;
        }
    }

    void setMTU(int mtu) {
        mMTU = Math.min(mtu, mMTULimit);
    }

    public void handlePPoGATTPacket(byte[] value) {
        if (!mIsConnected) {
            mIsConnected = true;
            synchronized (this) {
                this.notify();
            }
        }
        //LOG.info("write request: offset = " + offset + " value = " + GB.hexdump(value, 0, -1));
        int header = value[0] & 0xff;
        int command = header & 7;
        int serial = header >> 3;
        if (command == 0x01) {
            LOG.info("got ACK for serial = " + serial);
        }
        if (command == 0x02) { // some request?
            LOG.info("got command 0x02");
            if (value.length > 1) {
                sendDataToPebble(new byte[]{0x03, 0x19, 0x19}); // no we don't know what that means
                createPipedInputReader(); // FIXME: maybe not here
            } else {
                sendDataToPebble(new byte[]{0x03}); // no we don't know what that means
            }
        } else if (command == 0) { // normal package
            LOG.info("got PPoGATT package serial = " + serial + " sending ACK");

            sendAckToPebble(serial);

            writeToPipedOutputStream(value, 1, value.length - 1);
        }
    }

    private void sendAckToPebble(int serial) {
        sendDataToPebble(new byte[]{(byte) (((serial << 3) | 1) & 0xff)});
    }

    private synchronized void sendDataToPebble(final byte[] bytes) {
        if (mPebbleGATTServer != null) {
            mWriteHandler.post(new Runnable() {
                @Override
                public void run() {
                    mPebbleGATTServer.sendDataToPebble(bytes);
                }
            });
        } else {
            // For now only in experimental client only code
            mWriteHandler.post(new Runnable() {
                @Override
                public void run() {
                    mPebbleGATTClient.sendDataToPebble(bytes);
                }
            });
        }
    }

    private class PipeReader extends Thread {
        int mmSequence = 0;

        @Override
        public void run() {
            byte[] buf = new byte[8192];
            int bytesRead;
            while (true) {
                try {
                    // this code is very similar to iothread, that is bad
                    // because we are the ones who prepared the buffer, there should be no
                    // need to do crazy stuff just to find out the PP boundaries again.
                    bytesRead = mPipedInputStream.read(buf, 0, 4);
                    while (bytesRead < 4) {
                        bytesRead += mPipedInputStream.read(buf, bytesRead, 4 - bytesRead);
                    }

                    int length = (buf[0] & 0xff) << 8 | (buf[1] & 0xff);
                    bytesRead = mPipedInputStream.read(buf, 4, length);

                    while (bytesRead < length) {
                        bytesRead += mPipedInputStream.read(buf, bytesRead + 4, length - bytesRead);
                    }


                    int payloadToSend = bytesRead + 4;
                    int srcPos = 0;
                    while (payloadToSend > 0) {
                        int chunkSize = (payloadToSend < (mMTU - 4)) ? payloadToSend : mMTU - 4;
                        byte[] outBuf = new byte[chunkSize + 1];
                        outBuf[0] = (byte) ((mmSequence++ << 3) & 0xff);
                        System.arraycopy(buf, srcPos, outBuf, 1, chunkSize);
                        sendDataToPebble(outBuf);
                        srcPos += chunkSize;
                        payloadToSend -= chunkSize;
                    }
                } catch (IOException e) {
                    LOG.info(e.getMessage());
                    Thread.currentThread().interrupt();
                    break;
                }
            }
            LOG.info("Pipereader thread shut down");
        }

        @Override
        public void interrupt() {
            super.interrupt();
            try {
                LOG.info("closing piped inputstream");
                mPipedInputStream.close();
            } catch (IOException ignore) {
            }
        }
    }

    boolean isExpectedDevice(BluetoothDevice device) {
        if (!device.getAddress().equals(mBtDevice.getAddress())) {
            LOG.info("unhandled device: " + device.getAddress() + " , ignoring, will only talk to " + mBtDevice.getAddress());
            return false;
        }
        return true;
    }
}

