package com.leerybit.escpos.bluetooth;

import android.app.Activity;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.UUID;

import com.leerybit.escpos.DeviceCallbacks;
import com.leerybit.escpos.R;

/**
 * Copyright 2015 LeeryBit
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
public class BTService {

  ConnectThread connectThread;
  ListenThread listenThread;
  MainThread mainThread;

  private static final String TAG = "BTService";
  private static final String BT_NAME = "BTService";
  @SuppressWarnings("SpellCheckingInspection")
  private static final UUID BT_UUID = UUID.fromString("B3EDED23-82FB-4054-B24F-043966975FCA");
  private static final int REQUEST_ENABLE_BLUETOOTH = 0x0BBA;
  public static final int STATE_NONE = 0x00;
  public static final int STATE_LISTENING = 0x01;
  public static final int STATE_CONNECTING = 0x02;
  public static final int STATE_CONNECTED = 0x03;

  public static final int MESSAGE_STATE_CHANGE = 0x05;
  public static final int MESSAGE_READ = 0x06;
  public static final int MESSAGE_WRITE = 0x07;
  public static final int MESSAGE_CONNECTION_LOST = 0x08;
  public static final int MESSAGE_UNABLE_TO_CONNECT = 0x09;

  private final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
  private final Handler handler;
  private final Handler mainHandler;
  private final Context context;

  private int state;
  private BTCallback btCallback;
  private DeviceCallbacks deviceCallbacks;

  public BTService(Context context, Handler handler) {
    this.context = context;
    if (handler == null) {
      this.handler = new Handler();
    } else {
      this.handler = handler;
    }

    mainHandler = new Handler(Looper.getMainLooper());
    setState(STATE_NONE);
  }

  public synchronized boolean isAvailable() {
    return adapter != null;
  }

  public synchronized boolean isEnabled() {
    return isAvailable() && this.adapter.isEnabled();
  }

  public synchronized int getState() {
    return state;
  }

  void setState(int state) {
    setState(MESSAGE_STATE_CHANGE, state);
  }

  void setState(int message, int state) {
    this.state = state;
    handler.obtainMessage(-1, state, message).sendToTarget();
  }

  private void sendMessage(int message, int count, byte[] buffer) {
    handler.obtainMessage(count, getState(), message, buffer).sendToTarget();
  }

  public void setDeviceCallbacks(DeviceCallbacks callbacks) {
    this.deviceCallbacks = callbacks;
  }

  public synchronized boolean startDiscovery() {
    setState(STATE_LISTENING);
    return adapter.startDiscovery();
  }

  public synchronized boolean isDiscovering() {
    return adapter.isDiscovering();
  }

  public synchronized boolean cancelDiscovery() {
    return adapter.cancelDiscovery();
  }

  public synchronized Set<BluetoothDevice> getBondedDevices() {
    return adapter.getBondedDevices();
  }

  public synchronized void sendMessage(String message, String charset) {
    if (message.length() > 0) {
      byte[] send;
      try {
        send = message.getBytes(charset);
      } catch (UnsupportedEncodingException var5) {
        send = message.getBytes();
      }

      this.send(send);
      byte[] tail = new byte[]{(byte) 10, (byte) 13, (byte) 0};
      this.send(tail);
    }

  }

  public void send(byte[] out) {
    BTService.ListenThread r;
    synchronized (this) {
      if (this.state != 3) {
        return;
      }

      r = this.listenThread;
    }

    r.send(out);
  }

  @SuppressWarnings("UnusedParameters")
  public void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
      case REQUEST_ENABLE_BLUETOOTH:
        if (btCallback != null)
          btCallback.onEnableBTResult(isEnabled());
        break;
    }
  }

  public void requestBTActivation(Activity activity, BTCallback callback) {
    btCallback = callback;

    Intent turnOnIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    activity.startActivityForResult(turnOnIntent, REQUEST_ENABLE_BLUETOOTH);
  }

  private <T extends BaseThread> T cleanThread(T thread) {
    if (thread != null) {
      thread.cancel();
    }

    return null;
  }

  private void cancelAllThreads() {
    connectThread = cleanThread(connectThread);
    listenThread = cleanThread(listenThread);
    mainThread = cleanThread(mainThread);
  }

  public void start() {
    if (!isEnabled()) return;
    connectThread = cleanThread(connectThread);
    listenThread = cleanThread(listenThread);

    if (mainThread == null) {
      mainThread = new MainThread();
      mainThread.start();
    }
  }

  public void stop() {
    setState(STATE_NONE);
    cancelAllThreads();
  }

  public synchronized void connect(BluetoothDevice device) {
    listenThread = cleanThread(listenThread);
    if (state == STATE_CONNECTING && connectThread != null) {
      connectThread = cleanThread(connectThread);
    }

    connectThread = new ConnectThread(device);
    connectThread.start();
    setState(STATE_CONNECTING);
  }

  private synchronized void onDeviceConnected(BluetoothSocket socket, BluetoothDevice device) {
    cancelAllThreads();

    listenThread = new ListenThread(socket);
    listenThread.start();

    listenThread = new ListenThread(socket);
    listenThread.start();

    if (deviceCallbacks != null) {
      mainHandler.post(new Runnable() {
        @Override
        public void run() {
          deviceCallbacks.onConnected();
        }
      });
    }
    Log.e(TAG, "Device " + device.getName() + " [" + device.getAddress() + "] — connected");
    this.setState(STATE_CONNECTED);
  }

  private void onConnectingFailed() {
    this.setState(STATE_NONE);
    setState(MESSAGE_UNABLE_TO_CONNECT, getState());

  }

  private void onConnectionLost() {
    setState(MESSAGE_CONNECTION_LOST, getState());
  }

  private abstract class BaseThread extends Thread {
    public abstract void cancel();
  }

  private class MainThread extends BaseThread {
    private final BluetoothServerSocket serverSocket;

    public MainThread() {
      Log.d(TAG, "Main thread created");
      BluetoothServerSocket tmp = null;

      try {
        tmp = adapter.listenUsingRfcommWithServiceRecord(BT_NAME, BT_UUID);
      } catch (IOException ignored) {
      }

      serverSocket = tmp;
    }

    public void run() {
      Log.d(TAG, "Main thread started");
      this.setName("BTMainThread");
      BluetoothSocket socket;

      while (BTService.this.getState() != STATE_CONNECTED) {
        try {
          socket = serverSocket.accept();
        } catch (IOException e) {
          break;
        }

        if (socket != null) {
          synchronized (BTService.this) {
            switch (BTService.this.getState()) {
              case STATE_NONE:
              case STATE_CONNECTED:
                try {
                  socket.close();
                } catch (IOException ignored) {
                }
                break;
              case STATE_LISTENING:
              case STATE_CONNECTING:
                onDeviceConnected(socket, socket.getRemoteDevice());
            }
          }
        }
      }
    }

    @Override
    public void cancel() {
      Log.d(TAG, "Main thread cancelled");
      try {
        this.serverSocket.close();
      } catch (IOException ignored) {
      }
    }
  }

  private class ConnectThread extends BaseThread {
    private final BluetoothSocket socket;
    private final BluetoothDevice device;
    private final ProgressDialog dialog;

    @SuppressWarnings("TryWithIdenticalCatches")
    public ConnectThread(@NonNull BluetoothDevice device) {
      Log.d(TAG, "Connect thread created");
      this.device = device;

      dialog = new ProgressDialog(context);
      showDialog();

      BluetoothSocket tmp = null;
      try {
        //noinspection RedundantArrayCreation, SpellCheckingInspection
        Method m = device.getClass().getMethod("createRfcommSocket", new Class[]{int.class});
        try {
          tmp = (BluetoothSocket) m.invoke(device, 1);
        } catch (InvocationTargetException ignored) {
        }
      } catch (IllegalAccessException ignored) {
      } catch (NoSuchMethodException ignored) {
      }

      socket = tmp;
    }

    @Override
    public void run() {
      Log.d(TAG, "Connect thread started");
      this.setName("BTConnectThread");
      adapter.cancelDiscovery();

      try {
        this.socket.connect();
      } catch (IOException e) {
        Log.e("BTService", "FAILED");
        e.printStackTrace();
        onConnectingFailed();
        sendFail();

        try {
          this.socket.close();
        } catch (IOException ignored) {
        }

        BTService.this.start();
        hideDialog();
        return;
      }

      synchronized (BTService.this) {
        connectThread = null;
      }

      onDeviceConnected(this.socket, this.device);
      hideDialog();
    }

    private void sendFail() {
      if (deviceCallbacks != null) {
        mainHandler.post(new Runnable() {
          @Override
          public void run() {
            deviceCallbacks.onFailure();
          }
        });
      }
    }

    private void showDialog() {
      String deviceName = device.getName();
      if (deviceName == null || deviceName.isEmpty()) deviceName = device.getAddress();
      String title = context.getString(R.string.pos_title_connecting_to) + " " + deviceName;
      dialog.setMessage(title);
      dialog.setCancelable(false);

      mainHandler.post(new Runnable() {
        @Override
        public void run() {
          dialog.show();
        }
      });
    }

    private void hideDialog() {
      mainHandler.post(new Runnable() {
        @Override
        public void run() {
          dialog.hide();
        }
      });
    }

    @Override
    public void cancel() {
      Log.d(TAG, "Connect thread cancelled");
      try {
        socket.close();
      } catch (IOException ignored) {
      }
    }
  }

  private class ListenThread extends BaseThread {
    private final BluetoothSocket socket;
    private final InputStream inStream;
    private final OutputStream outStream;

    public ListenThread(@NonNull BluetoothSocket socket) {
      Log.d(TAG, "Listen thread created");
      this.socket = socket;

      InputStream tmpIn = null;
      OutputStream tmpOut = null;

      try {
        tmpIn = socket.getInputStream();
        tmpOut = socket.getOutputStream();
      } catch (IOException ignored) {
      }

      this.inStream = tmpIn;
      this.outStream = tmpOut;
    }

    @Override
    public void run() {
      Log.d(TAG, "Listen thread started");
      this.setName("BTConnectedThread");

      try {
        while (true) {
          byte[] e = new byte[256];
          int bytes = inStream.read(e);
          if (bytes <= 0) {
            fail();
            break;
          }

          sendMessage(MESSAGE_READ, bytes, e);
        }
      } catch (IOException e) {
        fail();
      }
    }

    private void fail() {
      onConnectionLost();
      if (BTService.this.getState() != STATE_NONE) {
        BTService.this.start();
      }
    }

    public void send(byte[] buffer) {
      try {
        outStream.write(buffer);
        sendMessage(MESSAGE_WRITE, buffer.length, buffer);
      } catch (IOException ignored) {
      }
    }

    @Override
    public void cancel() {
      Log.d(TAG, "Listen thread cancelled");
      try {
        socket.close();
      } catch (IOException ignored) {
      }
    }
  }

  public interface BTCallback {
    void onEnableBTResult(boolean isEnabled);
  }
}