package my.lan.zooty;

import android.app.Activity;
import android.util.Log;
import android.content.Intent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.io.ByteArrayOutputStream;

public class AardRemoteActivity extends Activity
{
    // Debugging
    private static final String TAG = "AardRemote";
    private static final boolean D = true;

    // Message types sent from the AardTalkerService Handler
    public static final int MESSAGE_STATE_CHANGE = 1;
    public static final int MESSAGE_READ = 2;
    public static final int MESSAGE_WRITE = 3;
    public static final int MESSAGE_DEVICE_NAME = 4;
    public static final int MESSAGE_TOAST = 5;

    // Key names received from the AardTalkerService Handler
    public static final String DEVICE_NAME = "device_name";
    public static final String TOAST = "toast";

    // Constants for AardRemote protocol messages
    public static final int MK_INVALID      = 0;
    public static final int MK_COMMAND      = 0x40;
    public static final int MK_REPLY        = 0x80;
    public static final int MK_ASYNC        = 0xc0;
    public static final int MKCR_INVALID    = 0x00;
    public static final int MKR_NACK        = 0x01;
    public static final int MKCR_QVERSION   = 0x02;
    public static final int AARDREMOTE_CURRENT_VERSION = 9;
    public static final int MKCR_RECEIVER   = 0x03;
    public static final int MKCR_QRECEIVER  = 0x04;
    public static final int MKCR_FREQUENCY  = 0x05;
    public static final int MKCR_QFREQUENCY = 0x06;
    public static final int MKCR_QMICROS    = 0x07;
    public static final int MKCR_SENDRAW    = 0x08;
    public static final int MKCR_WAITAVAIL  = 0x09;
    public static final int MKCR_WAITSEND   = 0x0A;
    public static final int MKCR_QUEUESIZE  = 0x0B;
    public static final int MKCR_RESET      = 0x0C;
    public static final int MKCR_EXTENDED   = 0xff;
    public static final int MKAS_INVALID    = 0x00;
    public static final int MKAS_RESET      = 0xfb;
    public static final int MKAS_BILGE      = 0xfc;
    public static final int MKAS_GLOBERR    = 0xfd;
    public static final int MKAS_RAWIR      = 0xfe;
    public static final int MKAS_EXTENDED   = 0xff;


    // Name of the connected device
    private String mConnectedDeviceName = null;

    // Intent request codes
    private static final int REQUEST_CONNECT_DEVICE = 1;

    EditText log_text = null;
    Button   learn_b = null;
    Button   send_b = null;
    private BluetoothAdapter mBluetoothAdapter = null;
    // Member object for the chat services
    private AardTalkerService mTalkService = null;

    private ByteArrayOutputStream accum = new ByteArrayOutputStream();
    private int accum_state = 0;
    private int accum_size = 0;
    private int accum_need = 0;

    private ByteArrayOutputStream rawir = new ByteArrayOutputStream();
    private int rawir_len = 0;
    private boolean rawir_done = false;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        if(D) Log.d(TAG, "ON CREATE 1");
        setContentView(R.layout.main);
        if(D) Log.d(TAG, "ON CREATE 2");

        // Get local Bluetooth adapter
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if(D) Log.d(TAG, "ON CREATE 3");

        log_text = (EditText) findViewById(R.id.log_text);

        ((Button) findViewById(R.id.connect_button)).setOnClickListener(
            new Button.OnClickListener() {
                @Override public void onClick(View v) {
                    doConnect();
                } });

        learn_b = (Button) findViewById(R.id.learn_button);
        learn_b.setOnClickListener(
            new Button.OnClickListener() {
                @Override public void onClick(View v) {
                    doLearn();
                } });

        send_b = (Button) findViewById(R.id.send_button);
        send_b.setOnClickListener(
            new Button.OnClickListener() {
                @Override public void onClick(View v) {
                    doSend();
                } });

        learn_b.setClickable(false);
        send_b.setClickable(false);
    }

    void doConnect() {
       log_text.append("doConnect called\n");
       if(D) Log.d(TAG, "doConnect 1");
       // Launch the DeviceListActivity to see devices
       Intent serverIntent = new Intent(this, DeviceListActivity.class);
       if(D) Log.d(TAG, "doConnect 2");
       startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
       if(D) Log.d(TAG, "doConnect 3");
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CONNECT_DEVICE) {
           if (resultCode == Activity.RESULT_OK) {

              // Get the device MAC address
              String address =
                 data.getExtras().getString(
                                     DeviceListActivity.EXTRA_DEVICE_ADDRESS);

              // Get the BLuetoothDevice object
              BluetoothDevice device =
                 mBluetoothAdapter.getRemoteDevice(address);

              // Attempt to connect to the device
              if (mTalkService == null) {
                 mTalkService = new AardTalkerService(this, mHandler);
              }
              mTalkService.connect(device);
           }
        }
    }

    // The Handler that gets information back from the AardTalkerService
    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MESSAGE_STATE_CHANGE:
                if(D) Log.i(TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1);
                switch (msg.arg1) {
                case AardTalkerService.STATE_CONNECTED:
                    log_text.append("connected\n");
                    {
                       byte[] bad = new byte[1];
                       bad[0] = 47;
                       mTalkService.write(bad);
                    }
                    break;
                case AardTalkerService.STATE_CONNECTING:
                    log_text.append("connecting...\n");
                    break;
                case AardTalkerService.STATE_NONE:
                    log_text.append("not connected\n");
                    break;
                }
                break;
            case MESSAGE_WRITE:
                {

                // The BluetoothChat service worked this way, so I copied
                // it - the service sends me back a message with the data
                // it just wrote (even though you'd think I know what I
                // called write() with :-). I take advantage of this
                // message to log the written data to the log window.

                byte[] writeBuf = (byte[]) msg.obj;
                // construct a string from the buffer
                StringBuilder sb = new StringBuilder();
                sb.append("->");
                for (byte b : writeBuf) {
                   sb.append(String.format(" %02X", b));
                }
                sb.append("\n");
                log_text.append(sb.toString());
                }
                break;
            case MESSAGE_READ:
                {
                byte[] readBuf = (byte[]) msg.obj;
                int len = msg.arg1;
                int curoff = 0;
                byte t;
                byte cc;
                int wordlen;
                int bytelen;

                // The readBuf now contains len bytes of raw data. I need
                // to accumulate complete AardRemote protocol messages by
                // gathering appropriate dribs and drabs of this data.
                // I do this by using the accum ByteArrayOutputStream
                // to accumulate one complete message which is then processed
                // as a whole. The readBuf may contain more than one
                // AardRemote message, or a fraction of one message, and
                // I need to get the accumulation correct whatever shows
                // up here.

                while (len > 0) {
                   switch (accum_state) {
                   case 0:
                      // In state 0, there is nothing in accum and the data
                      // in readBuf should begin with a message header.  A
                      // proper message from the AardRemote should be
                      // flagged as either MK_REPLY or MK_ASYNC with the
                      // length stored in the low 6 bits of the byte.  (The
                      // length units are number of 16 bit words, so the
                      // byte length is twice that). A length of zero means
                      // a variable length message terminated with a two
                      // byte zero word.
                      cc = readBuf[curoff];
                      t = (byte)(((int)cc) & ((int)0xc0) & 0xff);
                      if ((t == (byte)MK_REPLY) || (t == (byte)MK_ASYNC)) {
                         // Looks like a valid message header.
                         wordlen = cc & 0x3f;
                         if (wordlen != 0) {
                            // Fixed length message. If I have it all in the
                            // buffer already I can just suck it all up now
                            // and process it, otherwise I have to wait for
                            // the next chunk to come in.
                            bytelen = wordlen * 2;
                            if (bytelen <= len) {
                               accum.write(readBuf, curoff, bytelen);
                               curoff += bytelen;
                               len -= bytelen;
                               processIncoming(accum.toByteArray(), bytelen);
                               accum.reset();
                               accum_size = 0;
                               accum_need = 0;
                               accum_state = 0;
                            } else {
                               accum.write(readBuf, curoff, len);
                               accum_size += len;
                               curoff += len;
                               accum_need = bytelen - len;
                               len = 0;
                               accum_state = 1;
                            }
                         } else {
                            // Gah! I have one of those nasty variable length
                            // messages, need to look for a properly
                            // word aligned 0,0 byte pair in incoming message
                            // to know how long it is.
                            accum_state = 2;
                         }
                      } else {
                         // We are seriously out of sync, discard all data
                         // in buffer and hope the next chunk we read will
                         // make sense and we'll get back in sync.
                         len = 0;
                         accum.reset();
                         accum_state = 0;
                         accum_size = 0;
                         accum_need = 0;
                      }
                      break;
                   case 1:
                      // In state 1 I have accumulated accum_size bytes in
                      // the accum buffer and I need accum_need more bytes
                      // to have a complete message.
                      if (len >= accum_need) {
                         accum.write(readBuf, curoff, accum_need);
                         len -= accum_need;
                         curoff += accum_need;
                         accum_size += accum_need;
                         processIncoming(accum.toByteArray(), accum_size);
                         accum_need = 0;
                         accum.reset();
                         accum_size = 0;
                         accum_state = 0;
                      } else {
                         accum.write(readBuf, curoff, len);
                         curoff += len;
                         accum_size += len;
                         accum_need -= len;
                         accum_state = 1;
                         len = 0;
                      }
                      break;
                   case 2:
                      // In state 2 I have accumulated accum_size bytes in
                      // the accum buffer and I need to see an even index offset
                      // zero byte followed by an odd index offset zero byte to
                      // say I have reached the end of the buffer.
                      log_text.append("I can't handle a var sized message!");
                      len = 0;
                      accum.reset();
                      accum_state = 0;
                      accum_size = 0;
                      accum_need = 0;
                      break;
                   }
                }
                }
                break;
            case MESSAGE_DEVICE_NAME:
                // save the connected device's name
                mConnectedDeviceName = msg.getData().getString(DEVICE_NAME);
                log_text.append("connected to device ");
                log_text.append(mConnectedDeviceName);
                log_text.append("\n");
                Toast.makeText(getApplicationContext(), "Connected to "
                               + mConnectedDeviceName, Toast.LENGTH_SHORT).show();
                break;
            case MESSAGE_TOAST:
                Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST),
                               Toast.LENGTH_SHORT).show();
                break;
            }
        }
    };

    void processIncoming(byte[] inbuf, int len) {
       // This is the exceptionally crude processor of responses from the
       // AardRemote. It needs to be vastly more complex and reliable in the
       // final version (with timeouts and such) and I should generate
       // predefined constants for this Java code from the #defines in
       // the C code rather than turning everything into "known" integer
       // values like this does, but for this hacky test,
       // it just does some really simple stuff.

       StringBuilder sb = new StringBuilder();
       sb.append("<-");
       for (int i = 0; i < len; i++) {
          byte b = inbuf[i];
          sb.append(String.format(" %02X", b));
       }
       sb.append("\n");
       log_text.append(sb.toString());

       // The code that recognizes STATE_CONNECTED above sends out a bad
       // command to trigger a conversation with the remote. The first
       // response I get should be an error reporting the bad command.
       // So when I see that error, I'll make the "learn" button clickable.
       if ((len == 4) &&
           (inbuf[0] == (byte)(MK_ASYNC|2)) &&
           (inbuf[1] == (byte)MKAS_BILGE) &&
           (inbuf[2] == 47) &&
           (inbuf[3] == 0)) {
          learn_b.setClickable(true);
       }

       // When a remote code shows up, I want to record it in the form of a
       // send command I can use to send out that same code, and enable make
       // the send button clickable. This is kind of complicated because the
       // data shows up in dribs and drabs as several messages before the
       // end of the message is seen, so the dead simplest thing to do is
       // accumulate the pieces in an ArrayList then condense them back down
       // to a fixed byte array once I have a complete remote code.
       if (! rawir_done) {
          if ((((inbuf[0] & MK_ASYNC) & 0xff) == MK_ASYNC) &&
              ((inbuf[1] & 0xff) == MKAS_RAWIR)) {
             if (rawir_len == 0) {
                // This is the first bit of raw IR code, compose a header
                // in the rawir buffer that is a command to send raw IR data
                // (use a variable length message header since I don't know how
                // long it will be and it might be longer than will fit
                // in a fixed length message).
                byte[] hdr = new byte[2];
                hdr[0] = (byte)MK_COMMAND;
                hdr[1] = (byte)MKCR_SENDRAW;
                rawir.write(hdr, 0, 2);
                rawir_len = 2;
             }
             if (((inbuf[len-1] & 0xc0) & 0xff) == 0xc0) {
                len -= 2;
                rawir_done = true;
             }
             if (len > 2) {
                rawir.write(inbuf, 2, len-2);
             }
             if (rawir_done) {
                byte[] tail = new byte[2];
                tail[0] = 0;
                tail[1] = 0;
                rawir.write(tail, 0, 2);
                send_b.setClickable(true);
                // We have recorded a complete raw IR code to send back out.
                // We can turn off the receiver now.
                byte[] if_off = new byte[4];
                if_off[0] = (byte)(MK_COMMAND|2);
                if_off[1] = (byte)MKCR_RECEIVER;
                if_off[2] = 0;
                if_off[3] = 0;
                mTalkService.write(if_off);
             }
          }
       }
    }

    void doLearn() {
       log_text.append("doLearn called\n");
       // When the learn button is pressed, I want to enable the receiver
       // in the AardRemote.
       byte[] ir_on = new byte[4];
       ir_on[0] = (byte)(MK_COMMAND|2);
       ir_on[1] = (byte)MKCR_RECEIVER;
       ir_on[2] = 1;
       ir_on[3] = 0;
       mTalkService.write(ir_on);
    }

    void doSend() {
       log_text.append("doSend called\n");
       mTalkService.write(rawir.toByteArray());
    }
}
