/*
 * Copyright 2018 Daniel Underhay & Matthew Daley.
 *
 * This file is part of Walrus.
 *
 * Walrus is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Walrus 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Walrus.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.bugfuzz.android.projectwalrus.device.chameleonmini;

import android.content.Context;
import android.content.Intent;
import android.hardware.usb.UsbDevice;
import android.preference.PreferenceManager;
import android.support.annotation.Keep;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
import android.support.v7.app.AppCompatActivity;

import com.bugfuzz.android.projectwalrus.R;
import com.bugfuzz.android.projectwalrus.card.carddata.CardData;
import com.bugfuzz.android.projectwalrus.card.carddata.ISO14443ACardData;
import com.bugfuzz.android.projectwalrus.card.carddata.MifareCardData;
import com.bugfuzz.android.projectwalrus.device.CardDevice;
import com.bugfuzz.android.projectwalrus.device.LineBasedUsbSerialCardDevice;
import com.bugfuzz.android.projectwalrus.device.ReadCardDataOperation;
import com.bugfuzz.android.projectwalrus.device.UsbCardDevice;
import com.bugfuzz.android.projectwalrus.device.WriteOrEmulateCardDataOperation;
import com.bugfuzz.android.projectwalrus.device.chameleonmini.ui.ChameleonMiniRevGActivity;
import com.bugfuzz.android.projectwalrus.util.MiscUtils;
import com.felhr.usbserial.UsbSerialDevice;
import com.felhr.usbserial.UsbSerialInterface;
import com.google.common.primitives.Bytes;

import java.io.IOException;
import java.math.BigInteger;
import java.util.concurrent.Semaphore;
import java.util.logging.Logger;

@CardDevice.Metadata(
        name = "Chameleon Mini Rev.G",
        iconId = R.drawable.drawable_chameleon_mini_rev_g,
        supportsRead = {MifareCardData.class},
        supportsWrite = {},
        supportsEmulate = {MifareCardData.class}
)
@UsbCardDevice.UsbIds({@UsbCardDevice.UsbIds.Ids(vendorId = 0x16d0, productId = 0x4b2)})
public class ChameleonMiniRevGDevice extends LineBasedUsbSerialCardDevice
        implements CardDevice.Versioned {

    private final Semaphore semaphore = new Semaphore(1);

    @Keep
    public ChameleonMiniRevGDevice(Context context, UsbDevice usbDevice) throws IOException {
        super(context, usbDevice, "\r\n", "ISO-8859-1", context.getString(R.string.idle));
    }

    @Override
    protected void setupSerialParams(UsbSerialDevice usbSerialDevice) {
        usbSerialDevice.setBaudRate(115200);

        usbSerialDevice.setDataBits(UsbSerialInterface.DATA_BITS_8);
        usbSerialDevice.setParity(UsbSerialInterface.PARITY_NONE);
        usbSerialDevice.setStopBits(UsbSerialInterface.STOP_BITS_1);

        usbSerialDevice.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF);
    }

    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    private boolean tryAcquireAndSetStatus(String status) {
        if (!semaphore.tryAcquire()) {
            return false;
        }

        setStatus(status);
        return true;
    }

    private void releaseAndSetStatus() {
        setStatus(context.getString(R.string.idle));
        semaphore.release();
    }

    @Override
    @UiThread
    public void createReadCardDataOperation(AppCompatActivity activity,
            Class<? extends CardData> cardDataClass, int callbackId) {
        ensureOperationCreatedCallbackSupported(activity);

        ((OnOperationCreatedCallback) activity).onOperationCreated(new ReadMifareOperation(this),
                callbackId);
    }

    @Override
    @UiThread
    public void createWriteOrEmulateDataOperation(AppCompatActivity activity, CardData cardData,
            boolean write, int callbackId) {
        ensureOperationCreatedCallbackSupported(activity);

        ((OnOperationCreatedCallback) activity).onOperationCreated(
                new WriteOrEmulateMifareOperation(this, cardData, write), callbackId);
    }

    @Override
    public Intent getDeviceActivityIntent(Context context) {
        return ChameleonMiniRevGActivity.getStartActivityIntent(context, this);
    }

    @Override
    public String getVersion() throws IOException {
        if (!tryAcquireAndSetStatus(context.getString(R.string.getting_version))) {
            throw new IOException(context.getString(R.string.device_busy));
        }

        try {
            setReceiving(true);

            try {
                send("VERSION?");

                String version = receive(new WatchdogReceiveSink<String, String>(3000) {
                    private int state;

                    @Override
                    public String onReceived(String in) throws IOException {
                        switch (state) {
                            case 0:
                                if (!in.equals("101:OK WITH TEXT")) {
                                    throw new IOException(context.getString(
                                            R.string.command_error, "VERSION?", in));
                                }
                                ++state;
                                break;

                            case 1:
                                return in;
                        }
                        return null;
                    }
                });

                if (version == null) {
                    throw new IOException(context.getString(R.string.get_version_timeout));
                }

                return version;
            } finally {
                setReceiving(false);
            }
        } finally {
            releaseAndSetStatus();
        }
    }

    private static class ReadMifareOperation extends ReadCardDataOperation {

        ReadMifareOperation(CardDevice cardDevice) {
            super(cardDevice);
        }

        @Override
        @WorkerThread
        public void execute(final Context context,
                final ShouldContinueCallback shouldContinueCallback, final ResultSink resultSink)
                throws IOException {
            final ChameleonMiniRevGDevice chameleonMiniRevGDevice =
                    (ChameleonMiniRevGDevice) getCardDeviceOrThrow();

            if (!chameleonMiniRevGDevice.tryAcquireAndSetStatus(context.getString(R.string.reading))) {
                throw new IOException(context.getString(R.string.device_busy));
            }

            try {
                chameleonMiniRevGDevice.setReceiving(true);

                try {
                    chameleonMiniRevGDevice.send("CONFIG=ISO14443A_READER");

                    chameleonMiniRevGDevice.receive(new WatchdogReceiveSink<String, Void>(3000) {
                        private int state;

                        private short atqa;
                        private BigInteger uid;
                        private byte sak;

                        @Override
                        public Void onReceived(String in) throws IOException {
                            switch (state) {
                                case 0:
                                    if (!in.equals("100:OK")) {
                                        throw new IOException(context.getString(
                                                R.string.command_error, "CONFIG=", in));
                                    }

                                    chameleonMiniRevGDevice.send("TIMEOUT=2");

                                    ++state;
                                    break;

                                case 1:
                                    if (!in.equals("100:OK")) {
                                        throw new IOException(context.getString(
                                                R.string.command_error, "TIMEOUT=", in));
                                    }

                                    chameleonMiniRevGDevice.send("IDENTIFY");

                                    ++state;
                                    break;

                                case 2:
                                    switch (in) {
                                        case "101:OK WITH TEXT":
                                            ++state;
                                            break;

                                        case "203:TIMEOUT":
                                            resetWatchdog();
                                            chameleonMiniRevGDevice.send("IDENTIFY");
                                            break;

                                        default:
                                            throw new IOException(context.getString(
                                                    R.string.command_error, "IDENTIFY", in));
                                    }
                                    break;

                                case 3:
                                    ++state;
                                    break;

                                case 4:
                                    String[] lineAtqa = in.split(":");
                                    atqa = Short.reverseBytes((short) Integer.parseInt(
                                            lineAtqa[1].trim(), 16));

                                    ++state;
                                    break;

                                case 5:
                                    String[] lineUid = in.split(":");
                                    uid = new BigInteger(lineUid[1].trim(), 16);

                                    ++state;
                                    break;

                                case 6:
                                    String[] lineSak = in.split(":");
                                    sak = (byte) Integer.parseInt(lineSak[1].trim(), 16);

                                    resultSink.onResult(new MifareCardData(
                                            new ISO14443ACardData(atqa, uid, sak, null),
                                            null));

                                    if (!shouldContinueCallback.shouldContinue()) {
                                        break;
                                    }

                                    resetWatchdog();

                                    chameleonMiniRevGDevice.send("IDENTIFY");

                                    state = 2;
                                    break;
                            }

                            return null;
                        }

                        @Override
                        public boolean wantsMore() {
                            return shouldContinueCallback.shouldContinue();
                        }
                    });
                } finally {
                    chameleonMiniRevGDevice.setReceiving(false);
                }
            } finally {
                chameleonMiniRevGDevice.releaseAndSetStatus();
            }
        }

        @Override
        public Class<? extends CardData> getCardDataClass() {
            return MifareCardData.class;
        }
    }

    private static class WriteOrEmulateMifareOperation extends WriteOrEmulateCardDataOperation {

        WriteOrEmulateMifareOperation(CardDevice cardDevice, CardData cardData, boolean write) {
            super(cardDevice, cardData, write);
        }

        @Override
        @WorkerThread
        public void execute(final Context context,
                final ShouldContinueCallback shouldContinueCallback) throws IOException {
            if (isWrite()) {
                throw new RuntimeException("Can't write");
            }

            final ChameleonMiniRevGDevice chameleonMiniRevGDevice =
                    (ChameleonMiniRevGDevice) getCardDeviceOrThrow();

            if (!chameleonMiniRevGDevice.tryAcquireAndSetStatus(
                    context.getString(R.string.emulating))) {
                throw new IOException(context.getString(R.string.device_busy));
            }

            try {
                chameleonMiniRevGDevice.setReceiving(true);

                try {
                    MifareCardData mifareCardData = (MifareCardData) getCardData();
                    String cardType = mifareCardData.getTypeDetailInfo();
                    String chameleonMiniRevGConfig;
                    String chameleonMiniRevGSetting = "SETTING=";
                    String chameleonMiniRevGUpload = "UPLOAD";

                    // Set chameleon mini card slot to default value from preferences
                    // fall back value is 1
                    // TODO: prompt user for card slot or use default
                    int slot =
                            PreferenceManager.getDefaultSharedPreferences(context)
                                    .getInt(ChameleonMiniRevGActivity.SLOT_KEY,
                                            1);
                    chameleonMiniRevGDevice.send(chameleonMiniRevGSetting + slot);
                    String lineSetting = chameleonMiniRevGDevice.receive(1000);
                    if (lineSetting == null || !lineSetting.equals("100:OK")) {
                        throw new IOException(context.getString(
                                R.string.command_error, chameleonMiniRevGSetting, lineSetting));
                    }

                    // Check  type of card 1k or 4k.. and Issue chameleon mini config command
                    // TODO: This will change when mifareCardData can return card type
                    if (cardType.matches(".*Classic\\s1K.*")) {
                        chameleonMiniRevGConfig = "CONFIG=MF_CLASSIC_1K";
                    } else if (cardType.matches(".*Classic\\s4K.*")){
                        chameleonMiniRevGConfig = "CONFIG=MF_CLASSIC_4K";
                    } else {
                        throw new IOException("Failed to set Chameleon Mini Rev G card type using CONFIG= command");
                    }
                    chameleonMiniRevGDevice.send(chameleonMiniRevGConfig);
                    String lineConfig = chameleonMiniRevGDevice.receive(1000);
                    Logger.getAnonymousLogger().info("Response: " + lineConfig);
                    if (lineConfig == null || !lineConfig.equals("100:OK")) {
                        throw new IOException(context.getString(
                                R.string.command_error, chameleonMiniRevGConfig, lineConfig));
                    }

                    // Flatten card data into Mifare1k blob to send over XModem
                    byte[] mifare1k = new byte[0];
                    for (int i = 0; i < 64; ++i) {
                        MifareCardData.Block block = mifareCardData.getBlocks().get(i);
                        byte[] toAdd;
                        if (block != null) {
                            toAdd = block.data;
                        } else {
                            toAdd = new byte[16];
                        }
                        mifare1k = Bytes.concat(mifare1k, toAdd);
                    }
                    Logger.getAnonymousLogger().info("Mifare1k result: "
                            + MiscUtils.bytesToHex(mifare1k, false));

                    // Issue chameleon mini Upload command
                    chameleonMiniRevGDevice.send(chameleonMiniRevGUpload);
                    String lineUpload = chameleonMiniRevGDevice.receive(1000);
                    if (lineUpload == null || !lineUpload.equals("110:WAITING FOR XMODEM")) {
                        throw new IOException(context.getString(
                                R.string.command_error, chameleonMiniRevGUpload, lineUpload));
                    }

                    // Switch to bytewise mode and send Mifare1k card data to chameleon mini via XModem
                    chameleonMiniRevGDevice.setBytewise(true);
                    try {
                        int currentBlock = 1;
                        byte[] dataBlock;

                        while(true){
                            byte result = chameleonMiniRevGDevice.receiveByte(1000);

                            switch (result){
                                // if 21 = <NAK>
                                case 21 :
                                    break;

                                // if 6 = <ACK>
                                case 6 :
                                    currentBlock++;
                                    break;

                                default:
                                    throw new IOException("Unknown byte: " + result);
                            }

                            // Check if the current block is the last, if it is send EOT
                            int totalBlocks = mifare1k.length / 128;
                            if (currentBlock - 1 == totalBlocks) {
                                chameleonMiniRevGDevice.sendByte((byte) 0x04);
                                continue;
                            } else if (currentBlock - 1 >= totalBlocks) {
                                break;
                            }

                            // Send current block
                            chameleonMiniRevGDevice.sendByte((byte)0x01);
                            chameleonMiniRevGDevice.sendByte((byte)currentBlock);
                            chameleonMiniRevGDevice.sendByte((byte)(255 - currentBlock));
                            int i;
                            int checkSum = 0;
                            for (i = 0; i < 128; i++){
                                chameleonMiniRevGDevice.sendByte(mifare1k[(currentBlock - 1) * 128 + i]);
                                checkSum = checkSum + mifare1k[(currentBlock - 1) * 128 + i];
                            }
                            chameleonMiniRevGDevice.sendByte((byte)checkSum);
                        }
                    } finally {
                        chameleonMiniRevGDevice.setBytewise(false);
                    }
                } finally {
                    chameleonMiniRevGDevice.setReceiving(false);
                }
            } finally {
                chameleonMiniRevGDevice.releaseAndSetStatus();
            }
        }
    }
}