package com.giorgiofellipe.datecsprinter;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.Exception;
import java.util.Hashtable;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.UUID;
import java.net.Socket;
import java.net.UnknownHostException;
import java.lang.reflect.Method;

import android.app.Application;
import android.app.Activity;
import android.app.ProgressDialog;
import android.widget.Toast;
import android.util.Log;
import android.content.Intent;
import android.os.Handler;
import android.os.Bundle;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Base64;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import com.datecs.api.BuildInfo;
import com.datecs.api.printer.ProtocolAdapter;

public class DatecsSDKWrapper {
    private static final String LOG_TAG = "BluetoothPrinter";
    private Printer mPrinter;
    private ProtocolAdapter mProtocolAdapter;
    private BluetoothSocket mBluetoothSocket;
    private boolean mRestart;
    private String mAddress;
    private CallbackContext mConnectCallbackContext;
    private CallbackContext mCallbackContext;
    private ProgressDialog mDialog;
    private CordovaInterface mCordova;
    private CordovaWebView mWebView;
    private final Application app;

    /**
     * Interface de eventos da Impressora
     */
    private final ProtocolAdapter.PrinterListener mChannelListener = new ProtocolAdapter.PrinterListener() {
        @Override
        public void onPaperStateChanged(boolean hasNoPaper) {
            if (hasNoPaper) {
                sendStatusUpdate(true, false);
                showToast(DatecsUtil.getStringFromStringResource(app, "no_paper"));
            } else {
                sendStatusUpdate(true, true);
                showToast(DatecsUtil.getStringFromStringResource(app, "paper_ok"));
            }
        }

        @Override
        public void onThermalHeadStateChanged(boolean overheated) {
            if (overheated) {
                closeActiveConnections();
                sendStatusUpdate(false, false);
                showToast(DatecsUtil.getStringFromStringResource(app, "overheating"));
            }
        }

        @Override
        public void onBatteryStateChanged(boolean lowBattery) {
            sendStatusUpdate(true, true, lowBattery);
            showToast(DatecsUtil.getStringFromStringResource(app, "low_battery"));
        }
    };

    private Map<Integer, String> errorCode = new HashMap<Integer, String>();

    public DatecsSDKWrapper(CordovaInterface cordova) {
        mCordova = cordova;
        app = cordova.getActivity().getApplication();

        this.errorCode.put(1, DatecsUtil.getStringFromStringResource(app, "err_no_bt_adapter"));
        this.errorCode.put(2, DatecsUtil.getStringFromStringResource(app, "err_no_bt_device"));
        this.errorCode.put(3, DatecsUtil.getStringFromStringResource(app, "err_lines_number"));
        this.errorCode.put(4, DatecsUtil.getStringFromStringResource(app, "err_feed_paper"));
        this.errorCode.put(5, DatecsUtil.getStringFromStringResource(app, "err_print"));
        this.errorCode.put(6, DatecsUtil.getStringFromStringResource(app, "err_fetch_st"));
        this.errorCode.put(7, DatecsUtil.getStringFromStringResource(app, "err_fetch_tmp"));
        this.errorCode.put(8, DatecsUtil.getStringFromStringResource(app, "err_print_barcode"));
        this.errorCode.put(9, DatecsUtil.getStringFromStringResource(app, "err_print_test"));
        this.errorCode.put(10, DatecsUtil.getStringFromStringResource(app, "err_set_barcode"));
        this.errorCode.put(11, DatecsUtil.getStringFromStringResource(app, "err_print_img"));
        this.errorCode.put(12, DatecsUtil.getStringFromStringResource(app, "err_print_rect"));
        this.errorCode.put(13, DatecsUtil.getStringFromStringResource(app, "err_print_rect"));
        this.errorCode.put(14, DatecsUtil.getStringFromStringResource(app, "err_print_rect"));
        this.errorCode.put(15, DatecsUtil.getStringFromStringResource(app, "err_print_rect"));
        this.errorCode.put(16, DatecsUtil.getStringFromStringResource(app, "err_print_rect"));
        this.errorCode.put(17, DatecsUtil.getStringFromStringResource(app, "err_print_rect"));
        this.errorCode.put(18, DatecsUtil.getStringFromStringResource(app, "failed_to_connect"));
        this.errorCode.put(19, DatecsUtil.getStringFromStringResource(app, "err_bt_socket"));
        this.errorCode.put(20, DatecsUtil.getStringFromStringResource(app, "failed_to_initialize"));
        this.errorCode.put(21, DatecsUtil.getStringFromStringResource(app, "err_write"));
        this.errorCode.put(22, DatecsUtil.getStringFromStringResource(app, "err_print_qrcode"));
    }

    private JSONObject getErrorByCode(int code) {
        return this.getErrorByCode(code, null);
    }

    private JSONObject getErrorByCode(int code, Exception exception) {
        JSONObject json = new JSONObject();
        try {
            json.put("errorCode", code);
            json.put("message", errorCode.get(code));
            if (exception != null) {
                json.put("exception", exception.getMessage());
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, e.getMessage());
            showToast(e.getMessage());
        }
        return json;
    }

    /**
     * Busca todos os dispositivos Bluetooth pareados com o device
     *
     * @param callbackContext
     */
    protected void getBluetoothPairedDevices(CallbackContext callbackContext) {
        BluetoothAdapter mBluetoothAdapter = null;
        try {
            mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
            if (mBluetoothAdapter == null) {
                callbackContext.error(this.getErrorByCode(1));
                return;
            }
            if (!mBluetoothAdapter.isEnabled()) {
                Intent enableBluetooth = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                this.mCordova.getActivity().startActivityForResult(enableBluetooth, 0);
            }
            Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
            if (pairedDevices.size() > 0) {
                JSONArray json = new JSONArray();
                for (BluetoothDevice device : pairedDevices) {
                    Hashtable map = new Hashtable();
                    int deviceType = 0;
                    try {
                        Method method = device.getClass().getMethod("getType");
                        if (method != null) {
                            deviceType = (Integer) method.invoke(device);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    map.put("type", deviceType);
                    map.put("address", device.getAddress());
                    map.put("name", device.getName());
                    String deviceAlias = device.getName();
                    try {
                        Method method = device.getClass().getMethod("getAliasName");
                        if (method != null) {
                            deviceAlias = (String) method.invoke(device);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    map.put("aliasName", deviceAlias);
                    JSONObject jObj = new JSONObject(map);
                    json.put(jObj);
                }
                callbackContext.success(json);
            } else {
                callbackContext.error(this.getErrorByCode(2));
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, e.getMessage());
            e.printStackTrace();
            callbackContext.error(e.getMessage());
        }
    }

    /**
     * Seta em memória o endereço da impressora cuja conexão está sendo estabelecida
     *
     * @param address
     */
    protected void setAddress(String address) {
        mAddress = address;
    }

    protected void setWebView(CordovaWebView webView) {
        mWebView = webView;
    }

    // public void setCordova(CordovaInterface cordova) {
    //     mCordova = cordova;
    // }

    /**
     * CallbackContext de cada requisição, que efetivamente recebe os retornos dos métodos
     *
     * @param callbackContext
     */
    public void setCallbackContext(CallbackContext callbackContext) {
        mCallbackContext = callbackContext;
    }

    /**
     * Valida o endereço da impressora e efetua a conexão
     *
     * @param callbackContext
     */
    protected void connect(CallbackContext callbackContext) {
        mConnectCallbackContext = callbackContext;
        closeActiveConnections();
        if (BluetoothAdapter.checkBluetoothAddress(mAddress)) {
            establishBluetoothConnection(mAddress, callbackContext);
        }
    }

    /**
     * Encerra todas as conexões com impressoras e dispositivos Bluetooth ativas
     */
    public synchronized void closeActiveConnections() {
        closePrinterConnection();
        closeBluetoothConnection();
    }

    /**
     * Encerra a conexão com a impressora
     */
    private synchronized void closePrinterConnection() {
        if (mPrinter != null) {
            mPrinter.close();
        }

        if (mProtocolAdapter != null) {
            mProtocolAdapter.close();
        }
    }

    /**
     * Finaliza o socket Bluetooth e encerra todas as conexões
     */
    private synchronized void closeBluetoothConnection() {
        BluetoothSocket socket = mBluetoothSocket;
        mBluetoothSocket = null;
        if (socket != null) {
            try {
                Thread.sleep(50);
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Efetiva a conexão com o dispositivo Bluetooth
     *
     * @param address
     * @param callbackContext
     */
    private void establishBluetoothConnection(final String address, final CallbackContext callbackContext) {
        final DatecsSDKWrapper sdk = this;
        runJob(new Runnable() {
            @Override
            public void run() {
                BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
                BluetoothDevice device = adapter.getRemoteDevice(address);
                UUID uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
                InputStream in = null;
                OutputStream out = null;
                adapter.cancelDiscovery();

                try {
                    mBluetoothSocket = createBluetoothSocket(device, uuid, callbackContext);
                    Thread.sleep(50);
                    mBluetoothSocket.connect();
                    in = mBluetoothSocket.getInputStream();
                    out = mBluetoothSocket.getOutputStream();
                } catch (IOException e) {
                    //fallback
                    try {
                        mBluetoothSocket = (BluetoothSocket) device.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(device, 1);
                        Thread.sleep(50);
                        mBluetoothSocket.connect();
                        in = mBluetoothSocket.getInputStream();
                        out = mBluetoothSocket.getOutputStream();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                        callbackContext.error(sdk.getErrorByCode(18, ex));
                        return;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    callbackContext.error(sdk.getErrorByCode(18, e));
                    return;
                }

                try {
                    initializePrinter(in, out, callbackContext);
                    showToast(DatecsUtil.getStringFromStringResource(app, "printer_connected"));
                    sendStatusUpdate(true);
                } catch (IOException e) {
                    e.printStackTrace();
                    callbackContext.error(sdk.getErrorByCode(20));
                    return;
                }
            }
        }, DatecsUtil.getStringFromStringResource(app, "printer"), DatecsUtil.getStringFromStringResource(app, "connecting"));
    }

    /**
     * Cria um socket Bluetooth
     *
     * @param device
     * @param uuid
     * @param callbackContext
     * @return BluetoothSocket
     * @throws IOException
     */
    private BluetoothSocket createBluetoothSocket(BluetoothDevice device, UUID uuid, final CallbackContext callbackContext) throws IOException {
        try {
            Method method = device.getClass().getMethod("createRfcommSocketToServiceRecord", new Class[] { UUID.class });
            return (BluetoothSocket) method.invoke(device, uuid);
        } catch (Exception e) {
            e.printStackTrace();
            sendStatusUpdate(false);
            callbackContext.error(this.getErrorByCode(19));
            showError(DatecsUtil.getStringFromStringResource(app, "failed_to_comm") + ": " + e.getMessage(), false);
        }
        return device.createRfcommSocketToServiceRecord(uuid);
    }

    /**
     * Inicializa a troca de dados com a impressora
     * @param inputStream
     * @param outputStream
     * @param callbackContext
     * @throws IOException
     */
    protected void initializePrinter(InputStream inputStream, OutputStream outputStream, CallbackContext callbackContext) throws IOException {
        mProtocolAdapter = new ProtocolAdapter(inputStream, outputStream);
        if (mProtocolAdapter.isProtocolEnabled()) {
            mProtocolAdapter.setPrinterListener(mChannelListener);
            
            final ProtocolAdapter.Channel channel = mProtocolAdapter.getChannel(ProtocolAdapter.CHANNEL_PRINTER);
            
            mPrinter = new Printer(channel.getInputStream(), channel.getOutputStream());
        } else {
            mPrinter = new Printer(mProtocolAdapter.getRawInputStream(), mProtocolAdapter.getRawOutputStream());
        }


        mPrinter.setConnectionListener(new Printer.ConnectionListener() {
            @Override
            public void onDisconnect() {
                sendStatusUpdate(false);
            }
        });
        callbackContext.success();
    }

    /**
     * Alimenta papel à impressora (rola papel em branco)
     *
     * @param linesQuantity
     */
    public void feedPaper(int linesQuantity) {
        if (linesQuantity < 0 || linesQuantity > 255) {
            mCallbackContext.error(this.getErrorByCode(3));
        }
        try {
            mPrinter.feedPaper(linesQuantity);
            mPrinter.flush();
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(4, e));
        }
    }

    /**
     * Print text expecting markup formatting tags (default encoding is ISO-8859-1)
     *
     * @param text
     */
    public void printTaggedText(String text) {
        printTaggedText(text, "ISO-8859-1");
    }

    /**
     * Print text expecting markup formatting tags and a defined charset
     *
     * @param text
     * @param charset
     */
    public void printTaggedText(String text, String charset) {
        try {
            mPrinter.printTaggedText(text, charset);
            mPrinter.flush();
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(LOG_TAG, e.getMessage());
            mCallbackContext.error(this.getErrorByCode(5, e));
        }
    }

    /**
     * Converts HEX String into byte array and write
     *
     * @param String
     */
    public void writeHex(String s) {
        write(DatecsUtil.hexStringToByteArray(s));
    }

    /**
     * Writes all bytes from the specified byte array to this printer
     *
     * @param byte[]
     */
    public void write(byte[] b) {
        try {
            mPrinter.write(b);
            mPrinter.flush();
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(21, e));
        }
    }

    /**
     * Return what is the Printer current status
     */
    public void getStatus() {
        try {
            int status = mPrinter.getStatus();
            mCallbackContext.success(status);
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(6, e));
        }
    }

    /**
     * Return Printer's head temperature
     */
    public void getTemperature() {
        try {
            int temperature = mPrinter.getTemperature();
            mCallbackContext.success(temperature);
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(7, e));
        }
    }

    public void setBarcode(int align, boolean small, int scale, int hri, int height) {
        try {
            mPrinter.setBarcode(align, small, scale, hri, height);
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(10, e));
        }
    }

    /**
     * Print a Barcode
     *
     * @param type
     * @param data
     */
    public void printBarcode(int type, String data) {
        try {
            mPrinter.printBarcode(type, data);
            mPrinter.flush();
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(8, e));
        }
    }

    /**
     * Print a QRCode
     *
     * @param size - the size of symbol, value in {1, 4, 6, 8, 10, 12, 14}
     * @param eccLv - the error collection control level, where 1: L (7%), 2: M (15%), 3: Q (25%), 4: H (30%)
     * @param data - the QRCode data. The data must be between 1 and 448 symbols long.
     */
    public void printQRCode(int size, int eccLv, String data) {
        try {
            mPrinter.printQRCode(size, eccLv, data);
            mPrinter.flush();
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(22, e));
        }
    }


    /**
     * Print a selftest page
     */
    public void printSelfTest() {
        try {
            mPrinter.printSelfTest();
            mPrinter.flush();
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(9, e));
        }
    }

    public void drawPageRectangle(int x, int y, int width, int height, int fillMode) {
        try {
            mPrinter.drawPageRectangle(x, y, width, height, fillMode);
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(12, e));
        }
    }

    public void drawPageFrame(int x, int y, int width, int height, int fillMode, int thickness) {
        try {
            mPrinter.drawPageFrame(x, y, width, height, fillMode, thickness);
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(16, e));
        }
    }

    public void selectStandardMode() {
        try {
            mPrinter.selectStandardMode();
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(13, e));
        }
    }

    public void selectPageMode() {
        try {
            mPrinter.selectPageMode();
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(14, e));
        }
    }

    public void printPage() {
        try {
            mPrinter.printPage();
            mPrinter.flush();
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(17, e));
        }
    }

    public void setPageRegion(int x, int y, int width, int height, int direction) {
        try {
            mPrinter.setPageRegion(x, y, width, height, direction);
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(15, e));
        }
    }


    /**
     * Print an image
     *
     * @param image String (BASE64 encoded image)
     * @param width
     * @param height
     * @param align
     */
    public void printImage(String image, int width, int height, int align) {
        try {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inScaled = false;
            byte[] decodedByte = Base64.decode(image, 0);
            Bitmap bitmap = BitmapFactory.decodeByteArray(decodedByte, 0, decodedByte.length);
            final int imgWidth = bitmap.getWidth();
            final int imgHeight = bitmap.getHeight();
            final int[] argb = new int[imgWidth * imgHeight];

            bitmap.getPixels(argb, 0, imgWidth, 0, 0, imgWidth, imgHeight);
            bitmap.recycle();

            mPrinter.printImage(argb, width, height, align, true);
            mPrinter.flush();
            mCallbackContext.success();
        } catch (Exception e) {
            e.printStackTrace();
            mCallbackContext.error(this.getErrorByCode(11, e));
        }
    }

    /**
     * Wrapper para criação de Threads
     *
     * @param job
     * @param jobTitle
     * @param jobName
     */
    private void runJob(final Runnable job, final String jobTitle, final String jobName) {
        // Start the job from main thread
        mCordova.getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Progress dialog available due job execution
                final ProgressDialog dialog = new ProgressDialog(mCordova.getActivity());
                dialog.setTitle(jobTitle);
                dialog.setMessage(jobName);
                dialog.setCancelable(false);
                dialog.setCanceledOnTouchOutside(false);
                dialog.show();

                Thread t = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            job.run();
                        } finally {
                            dialog.dismiss();
                        }
                    }
                });
                t.start();
            }
        });
    }

    /**
     * Exibe Toast de erro
     *
     * @param text
     * @param resetConnection
     */
    private void showError(final String text, boolean resetConnection) {
        //we'l ignore toasts at the moment
    //    mCordova.getActivity().runOnUiThread(new Runnable() {
    //        @Override
    //        public void run() {
    //            Toast.makeText(mCordova.getActivity().getApplicationContext(), text, Toast.LENGTH_SHORT).show();
    //        }
    //    });
        if (resetConnection) {
            connect(mConnectCallbackContext);
        }
    }

    /**
     * Exibe mensagem Toast
     *
     * @param text
     */
    private void showToast(final String text) {
        //we'l ignore toasts at the moment
//        mCordova.getActivity().runOnUiThread(new Runnable() {
//            @Override
//            public void run() {
//                if (!mCordova.getActivity().isFinishing()) {
//                    Toast.makeText(mCordova.getActivity().getApplicationContext(), text, Toast.LENGTH_SHORT).show();
//                }
//            }
//        });
    }

    /**
     * Create a new plugin result and send it back to JavaScript
     *
     * @param connection status
     */
    private void sendStatusUpdate(boolean isConnected, boolean hasPaper, boolean lowBattery) {
        final Intent intent = new Intent("DatecsPrinter.connectionStatus");

        Bundle b = new Bundle();
        b.putBoolean("isConnected", isConnected);
        b.putBoolean("hasPaper", hasPaper);
        b.putBoolean("lowBattery", lowBattery);
        intent.putExtras(b);

        LocalBroadcastManager.getInstance(mWebView.getContext()).sendBroadcastSync(intent);
    }
    
    private void sendStatusUpdate(boolean isConnected, boolean hasPaper) {
        this.sendStatusUpdate(isConnected, hasPaper, false);
    }

    private void sendStatusUpdate(boolean isConnected) {
        this.sendStatusUpdate(isConnected, true, false);
    }
}