package com.datecs.lineademo;

import android.Manifest;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

import com.datecs.barcode.Barcode;
import com.datecs.barcode.Intermec;
import com.datecs.barcode.Newland;
import com.datecs.linea.LineaPro;
import com.datecs.linea.LineaProException;
import com.datecs.linea.LineaProInformation;
import com.datecs.lineademo.util.MediaUtil;
import com.datecs.lineademo.view.StatusView;
import com.datecs.lineaservice.LineaConnection;
import com.datecs.lineaservice.LineaManager;
import com.datecs.rfid.ContactlessCard;
import com.datecs.rfid.FeliCaCard;
import com.datecs.rfid.ISO14443Card;
import com.datecs.rfid.ISO15693Card;
import com.datecs.rfid.RC663;
import com.datecs.rfid.RFID;
import com.datecs.rfid.STSRICard;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;

import static com.datecs.lineademo.R.drawable.barcode;

public class MainActivity extends AppCompatActivity implements LineaPro.BarcodeListener, LineaPro
        .ButtonListener, LineaAction {

    private static final boolean DEBUG = true;

    private  static final int REQUEST_EXTERNAL_STORAGE_PERMISSION = 1;

    private static final int UPDATE_BATTERY_TIME = 3000;

    public interface LineaRunnable {
        void run(LineaPro linea) throws IOException;
    }

    private final LineaConnection mConnection = new LineaConnection() {
        @Override
        public void onLineaConnected(LineaPro linea) {
            MediaUtil.playSound(MainActivity.this, R.raw.connect);
            mStatusView.hide();
            mLineaPro = linea;
            mLineaPro.setBarcodeListener(MainActivity.this);
            mLineaPro.setButtonListener(MainActivity.this);
            mUpdateHandler.removeCallbacksAndMessages(null);
            mUpdateHandler.post(mUpdateRunnable);
            initLinea();
        }

        @Override
        public void onLineaDisconnected(LineaPro linea) {
            MediaUtil.playSound(MainActivity.this, R.raw.disconnect);
            mStatusView.show(R.drawable.usb_unplugged);
            mMainFragment.resetBattery();
            mUpdateHandler.removeCallbacksAndMessages(null);
        }

        @Override
        public void onLineaDebug(String message) {
            if (DEBUG) {
                mMainFragment.addLog(message);
            }
        }
    };

    private class UpdateRunnable implements Runnable {
        @Override
        public void run() {
            // Update battery information
            runAsync(new LineaRunnable() {
                @Override
                public void run(LineaPro linea) throws IOException {
                    final LineaPro.BatteryInfo batteryInfo = linea.getBatteryInfo();

                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mMainFragment.updateBattery(batteryInfo);
                        }
                    });
                }
            }, false);

            mUpdateHandler.postDelayed(this, UPDATE_BATTERY_TIME);
        }
    }

    private final Handler mUpdateHandler = new Handler();
    private final UpdateRunnable mUpdateRunnable = new UpdateRunnable();

    private SharedPreferences mPrefs;
    private LineaPro mLineaPro;
    private LineaManager mLineaManager;
    private MainFragment mMainFragment;
    private StatusView mStatusView;

    private final Object mSyncRoot = new Object();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);

        final FragmentManager fragmentManager = getFragmentManager();
        final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(R.id.content, mMainFragment = new MainFragment());
        fragmentTransaction.commit();

        mStatusView = (StatusView) findViewById(R.id.status_pane);
        mStatusView.show(R.drawable.usb_unplugged);

        mLineaManager = new LineaManager();

        grandExternalStoragePermission();
    }

    @Override
    protected void onStart() {
        super.onStart();
        mLineaManager.bindService(this, mConnection);
    }

    @Override
    protected void onStop() {
        super.onStop();
        mLineaManager.unbindService(this);
        mUpdateHandler.removeCallbacksAndMessages(null);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection
        switch (item.getItemId()) {
            case R.id.clear_log:
                mMainFragment.clearLog();
                break;
        }

        return true;
    }

    @Override
    public void onBackPressed() {
        final FragmentManager fragmentManager = getFragmentManager();

        if (fragmentManager.getBackStackEntryCount() > 0) {
            fragmentManager.popBackStack();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        if (requestCode == REQUEST_EXTERNAL_STORAGE_PERMISSION) {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

            }
        }
    }

    @Override
    public void onReadBarcode(final Barcode barcode) {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                if (mPrefs.getBoolean("beep_upon_scan", false)) {
                    linea.beep(100, new int[]{2730, 150, 65000, 20, 2730, 150});
                }

                if (mPrefs.getBoolean("vibrate_upon_scan", false)) {
                    linea.startVibrator(500);
                }
            }
        }, false);

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mMainFragment.addLog("<I>Barcode: (" + barcode.getTypeString() + ") " + barcode
                        .getDataString());
            }
        });
    }

    @Override
    public void onButtonStateChanged(int index, final boolean state) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (state) {
                    mStatusView.show(barcode);
                } else {
                    mStatusView.hide();
                }
            }
        });
    }

    @Override
    public void actionResetBarcodeEngine() {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                linea.bcRestoreDefaultMode();
            }
        }, true);
    }

    @Override
    public void actionUpdateSetting(final String key, final String value) {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                updateSetting(linea, key, value);
            }
        }, true);
    }

    @Override
    public void actionSetLed(final boolean red, final boolean green, final boolean blue) {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                final LineaProInformation info = linea.getInformation();

                if (info.hasLED()) {
                    linea.setLED(red, green, blue);
                }
            }
        }, true);
    }

    @Override
    public void actionUpdateFirmware(final String path, final int mode) {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                File file = new File(path);
                byte[] data = new byte[(int) file.length()];
                FileInputStream fis = new FileInputStream(file);
                if (fis.read(data) != data.length) {
                    throw new IOException("Can't read firmware file");
                }
                fis.close();

                LineaProInformation info = linea.getInformation();
                switch (mode) {
                    case 0:
                        linea.fwUpdate(data);
                        break;
                    case 1:
                        if (info.hasIntermecEngine()) {
                            Intermec engine = (Intermec) mLineaPro.bcGetEngine();
                            engine.updateFirmware(data, null);
                        } else {
                            warn("Barcode update is not supported");
                        }
                        break;
                    default: {
                        warn("Unsupported firmware update " + mode);
                    }
                }
            }
        }, true);
    }

    @Override
    public void actionReadTag() {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                RC663 rc663 = null;

                // Set RFID module debug output.
                RFID.setDebug(true);

                try {
                    final boolean[] status = { false };

                    // Create instance of RF module.
                    rc663 = linea.rfidGetModule();

                    // Register event to listen for cards presents
                    rc663.setCardListener(new RC663.CardListener() {
                        @Override
                        public void onCardDetect(ContactlessCard card) {
                            try {
                                processTag(card);
                            } catch (IOException e) {
                                e.printStackTrace();
                                fail("Tag processing failed: " + e.getMessage());
                            }

                            status[0] = true;

                            synchronized(mSyncRoot) {
                                mSyncRoot.notify();
                            }
                        }
                    });

                    // Enable RF module.
                    rc663.enable();

                    // Gives some time to complete operation.
                    synchronized (mSyncRoot) {
                        try {
                            mSyncRoot.wait(10000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    if (!status[0]) {
                        warn("No card detected");
                    }

                    // Disable RF module as soon as with finished with it to save power.
                    rc663.disable();
                } finally {
                    if (rc663 != null) {
                        rc663.close();
                    }
                }
            }
        }, true);
    }

    @Override
    public void actionTurnOff() {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                linea.turnOff();
            }
        }, false);
    }

    @Override
    public void actionStartScan() {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                linea.bcStartScan();
            }
        }, false);
    }

    @Override
    public void actionStopScan() {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                linea.bcStopScan();
            }
        }, false);
    }

    @Override
    public void actionReadInformation() {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                readInformation(linea);
            }
        }, false);
    }

    public void grandExternalStoragePermission() {
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {

            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    REQUEST_EXTERNAL_STORAGE_PERMISSION);
        }

        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {

            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    REQUEST_EXTERNAL_STORAGE_PERMISSION);
        }
    }

    private void info(final String text) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mMainFragment.addLog("<I>" + text);
            }
        });
    }

    private void warn(final String text) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mMainFragment.addLog("<W>" + text);
            }
        });
    }

    private void fail(final String text) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mMainFragment.addLog("<E>" + text);
            }
        });
    }

    private void runAsync(final LineaRunnable r, final boolean showProgress) {
        if (showProgress) {
            mStatusView.show(R.drawable.process);
        }

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                if (mLineaPro != null) {
                    try {
                        try {
                            r.run(mLineaPro);
                        } catch (LineaProException e) {
                            StringWriter sw = new StringWriter();
                            PrintWriter pw = new PrintWriter(sw);
                            e.printStackTrace(pw);
                            warn("Linea error: " + sw.toString());
                        } catch (IOException e) {
                            StringWriter sw = new StringWriter();
                            PrintWriter pw = new PrintWriter(sw);
                            e.printStackTrace(pw);
                            fail("I/O error: " + sw.toString());
                        } catch (Exception e) {
                            StringWriter sw = new StringWriter();
                            PrintWriter pw = new PrintWriter(sw);
                            e.printStackTrace(pw);
                            fail("Critical error: " + sw.toString());
                        }
                    } finally {
                        if (showProgress) {
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    mStatusView.hide();
                                }
                            });
                        }
                    }
                }
            }
        });
        thread.start();
    }

    private String byteArrayToHexString(byte[] buffer) {
        char[] tmp = new char[buffer.length * 3];

        for (int i = 0, j = 0; i < buffer.length; i++) {
            int a = (buffer[i] & 0xff) >> 4;
            int b = (buffer[i] & 0x0f);
            tmp[j++] = (char)(a < 10 ? '0' + a : 'A' + a - 10);
            tmp[j++] = (char)(b < 10 ? '0' + b : 'A' + b - 10);
            tmp[j++] = ' ';
        }

        return new String(tmp);
    }

    private void initLinea() {
        runAsync(new LineaRunnable() {
            @Override
            public void run(LineaPro linea) throws IOException {
                readInformation(linea);
                linea.bcStopScan();
                linea.bcStopBeep();

                updateSetting(linea, "scan_button");
                updateSetting(linea, "battery_charge");
                updateSetting(linea, "external_speaker");
                updateSetting(linea, "external_speaker_button");
                updateSetting(linea, "device_timeout_period");
                updateSetting(linea, "code128_symbology");
                updateSetting(linea, "barcode_scan_mode");
                updateSetting(linea, "barcode_scope_scale_mode");
            }
        }, true);
    }

    private void updateSetting(LineaPro linea, String key, String value) throws IOException {
        final LineaProInformation info = linea.getInformation();

        if ("scan_button".equals(key)) {
            boolean enabled = Boolean.parseBoolean(value);
            linea.enableScanButton(enabled);
        } else if ("battery_charge".equals(key)) {
            // Do not enable battery charge until device is in charging state.
            boolean enabled = Boolean.parseBoolean(value);
            linea.enableBatteryCharge(enabled);
        } else if ("external_speaker".equals(key)) {
            if (info.hasExternalSpeaker()) {
                boolean enabled = Boolean.parseBoolean(value);
                linea.enableExternalSpeaker(enabled);
            }
        } else if ("external_speaker_button".equals(key)) {
            if (info.hasExternalSpeaker()) {
                boolean enabled = Boolean.parseBoolean(value);
                linea.enableExternalSpeakerButton(enabled);
            }
        } else if ("device_timeout_period".equals(key)) {
            int autoOffTimeIndex = Integer.parseInt(value);
            if (autoOffTimeIndex == 0) {
                linea.setAutoOffTime(true, 30000);
            } else {
                linea.setAutoOffTime(true, 60000);
            }

        } else if ("code128_symbology".equals(key)) {
            if (info.hasIntermecEngine()) {
                Intermec engine = (Intermec) linea.bcGetEngine();
                if (engine != null) {
                    boolean enabled = Boolean.parseBoolean(value);
                    engine.enableCode128(enabled);
                    engine.saveSymbology();
                }
            }
        } else if ("barcode_scan_mode".equals(key)) {
            int scanMode = Integer.parseInt(value);
            linea.bcSetMode(scanMode);
        } else if ("barcode_scope_scale_mode".equals(key)) {
            if (info.hasNewlandEngine()) {
                int scopeScaleMode = Integer.parseInt(value);
                Newland engine = (Newland) linea.bcGetEngine();
                if (scopeScaleMode > 0) {
                    engine.turnOnScopeScaling(scopeScaleMode);
                } else {
                    engine.turnOffScopeScaling();
                }
            }
        }
    }

    private void updateSetting(LineaPro linea, String key) throws IOException {
        Object item = mPrefs.getAll().get(key);
        String value = item != null ? item.toString() : null;
        if (value != null) {
            updateSetting(linea, key, value);
        }
    }

    private void readInformation(LineaPro linea) throws IOException {
        // Read linea pro information
        final LineaProInformation info = linea.getInformation();
        final Object barcodeEngine = linea.bcGetEngine();
        final String barcodeIdent;
        if (info.hasIntermecEngine()) {
            String ident = "Intermec";
            try {
                Intermec engine = (Intermec) barcodeEngine;
                ident = "Intermec " + engine.getIdent();
            } catch (IOException e) {
                e.printStackTrace();
                ident = "Intermec";
            }
            barcodeIdent = ident;
        } else if (info.hasNewlandEngine()) {
            barcodeIdent = "Newland Barcode Engine";
        } else {
            barcodeIdent = "";
        }

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mMainFragment.addLog("<I>" + info.getName() + " " +
                        info.getModel() + " " + info.getVersion() + " " +
                        info.getSerialNumber());
                mMainFragment.addLog("<I>" + barcodeIdent);
            }
        });

    }

    private void processTag(ContactlessCard contactlessCard) throws IOException {
        if (contactlessCard instanceof ISO14443Card) {
            ISO14443Card card = (ISO14443Card)contactlessCard;
            info("ISO14 card: " + byteArrayToHexString(card.uid));


             // 16 bytes reading and 16 bytes writing
             // Try to authenticate first with default key
            byte[] key= new byte[] {-1, -1, -1, -1, -1, -1};
            // It is best to store the keys you are going to use once in the device memory,
            // then use AuthByLoadedKey function to authenticate blocks rather than having the key in your program
            card.authenticate('A', 8, key);

            // Write data to the card
            byte[] input = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
                    0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
            card.write16(8, input);

            // Read data from card
            byte[] result = card.read16(8);
            info("Data: " + byteArrayToHexString(result));

        } else if (contactlessCard instanceof ISO15693Card) {
            ISO15693Card card = (ISO15693Card)contactlessCard;
            info("ISO15 card: " + byteArrayToHexString(card.uid));
            info("Block size: " + card.blockSize);
            info("Max blocks: " + card.maxBlocks);

            /*
            if (card.blockSize > 0) {
                byte[] security = card.getBlocksSecurityStatus(0, 16);
                ...

                // Write data to the card
                byte[] input = new byte[] { 0x00, 0x01, 0x02, 0x03 };
                card.write(0, input);
                ...

                // Read data from card
                byte[] result = card.read(0, 1);
                ...
            }
            */
        } else if (contactlessCard instanceof FeliCaCard) {
            FeliCaCard card = (FeliCaCard)contactlessCard;
            info("FeliCa card: " + byteArrayToHexString(card.uid));

            /*
            // Write data to the card
            byte[] input = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
                    0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
            card.write(0x0900, 0, input);
            ...

            // Read data from card
            byte[] result = card.read(0x0900, 0, 1);
            ...
            */
        } else if (contactlessCard instanceof STSRICard) {
            STSRICard card = (STSRICard)contactlessCard;
            info("STSRI card: " + byteArrayToHexString(card.uid));
            info("Block size: " + card.blockSize);

            /*
            // Write data to the card
            byte[] input = new byte[] { 0x00, 0x01, 0x02, 0x03 };
            card.writeBlock(8, input);
            ...

            // Try reading two blocks
            byte[] result = card.readBlock(8);
            ...
            */
        } else {
            info("Contactless card: " + byteArrayToHexString(contactlessCard.uid));
        }
/*
        // Wait silently to remove card
        try {
            contactlessCard.waitRemove();
        } catch (IOException e) {
            e.printStackTrace();
        }
        */
    }
}
