package nordpol.android;

import android.nfc.Tag;
import android.nfc.NfcAdapter;
import android.nfc.tech.IsoDep;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.os.AsyncTask;
import android.os.Looper;
import android.os.Handler;
import android.widget.Toast;

/**
 * TagDispatcher provides a unified, simple and exclusive interface for NFC.
 * <p>
 * TagDispatcher provides an easy to use interface for getting tags no matter
 * the version of Android. With the interface in place you'll always receive
 * the tags the same way. While TagDispatcher is active ALL NFC activity will
 * go directly to your implementation.
 * <p>
 * TagDispatcher is set up using the
 * {@link TagDispatcherBuilder#TagDispatcherBuilder(Activity, OnDiscoveredTagListener)}. Depending on
 * your preferences you can call the methods in the builder to change the
 * default behavior.
 * <p>
 * To activate TagDispatcher after setting it up use the method
 * {@link #enableExclusiveNfc()}. To disable TagDispatcher use the method
 * {@link #disableExclusiveNfc()}.
 * <p>
 * For TagDispatcher to be able to handle tags on older Android versions
 * (pre-KITKAT) need to call {@link #interceptIntent(Intent)} from onNewIntent.
 * <p>
 * You'll be notified about new tags through the OnDiscoveredTagListener
 * interface that you provided when setting up TagDispatcher.
 */
public class TagDispatcher {
    private static final int DELAY_PRESENCE = 5000;

    private OnDiscoveredTagListener tagDiscoveredListener;
    private boolean handleUnavailableNfc;
    private boolean disableSounds;
    private boolean dispatchOnUiThread;
    private boolean broadcomWorkaround;
    private boolean noReaderMode;
    private boolean disableNdefCheck;
    private Activity activity;

    public enum NfcStatus {
       AVAILABLE_ENABLED,
       AVAILABLE_DISABLED,
       NOT_AVAILABLE
    }

    TagDispatcher(TagDispatcherBuilder tagDispatcherBuilder) {
      this.activity = tagDispatcherBuilder.activity;
      this.tagDiscoveredListener = tagDispatcherBuilder.tagDiscoveredListener;
      this.handleUnavailableNfc = tagDispatcherBuilder.enableUnavailableNfcUserPrompt;
      this.disableSounds = !tagDispatcherBuilder.enableSounds;
      this.dispatchOnUiThread = tagDispatcherBuilder.enableDispatchingOnUiThread;
      this.broadcomWorkaround = tagDispatcherBuilder.enableBroadcomWorkaround;
      this.noReaderMode = !tagDispatcherBuilder.enableReaderMode;
      this.disableNdefCheck = !tagDispatcherBuilder.enableNdefCheck;
    }

    TagDispatcher(Activity activity,
                  OnDiscoveredTagListener tagDiscoveredListener,
                  boolean handleUnavailableNfc,
                  boolean disableSounds,
                  boolean dispatchOnUiThread,
                  boolean broadcomWorkaround,
                  boolean noReaderMode,
                  boolean disableNdefCheck) {
        this.activity = activity;
        this.tagDiscoveredListener = tagDiscoveredListener;
        this.handleUnavailableNfc = handleUnavailableNfc;
        this.disableSounds = disableSounds;
        this.dispatchOnUiThread = dispatchOnUiThread;
        this.broadcomWorkaround = broadcomWorkaround;
        this.noReaderMode = noReaderMode;
        this.disableNdefCheck = disableNdefCheck;
    }

    /**
    * @deprecated  As of Nordpol 0.2, replaced by
    *              {@link TagDispatcherBuilder}.
    *              Will be removed in v0.3.0 or v1.0.0 (whichever comes first)
    *
    * @param activity               The Activity to attach the TagDispatcher to
    * @param tagDiscoveredListener  The interface for getting new tags
    * @param handleUnavailableNfc   If NFC is unavailable should Nordpol prompt
    *                               the user?
    * @param disableSounds          Should NFC interactions not produce sounds?
    * @param dispatchOnUiThread     Should tags be dispatched on the UI thread?
    * @param broadcomWorkaround     Should the Broadcom workaround be used?
    * @param noReaderMode           Shouldn't ReaderMode be used?
    * @param disableNdefCheck       Shouldn't NFC NDEF be checked?
    * @return                       A new TagDispatcher
    */
    public static TagDispatcher get(Activity activity,
                                    OnDiscoveredTagListener tagDiscoveredListener,
                                    boolean handleUnavailableNfc,
                                    boolean disableSounds,
                                    boolean dispatchOnUiThread,
                                    boolean broadcomWorkaround,
                                    boolean noReaderMode,
                                    boolean disableNdefCheck) {
        return new TagDispatcher(activity, tagDiscoveredListener, handleUnavailableNfc, disableSounds,
                                 dispatchOnUiThread, broadcomWorkaround, noReaderMode, disableNdefCheck);
    }

    /**
    * @deprecated  As of Nordpol 0.2, replaced by
    *              {@link TagDispatcherBuilder}.
    *              Will be removed in v0.3.0 or v1.0.0 (whichever comes first)
    *
    * @param activity               The Activity to attach the TagDispatcher to
    * @param tagDiscoveredListener  The interface for getting new tags
    * @param handleUnavailableNfc   If NFC is unavailable should Nordpol prompt
    *                               the user?
    * @param disableSounds          Should NFC interactions not produce sounds?
    * @param dispatchOnUiThread     Should tags be dispatched on the UI thread?
    * @param broadcomWorkaround     Should the Broadcom workaround be used?
    * @param noReaderMode           Shouldn't ReaderMode be used?
    * @return                       A new TagDispatcher
    */
    public static TagDispatcher get(Activity activity,
                                    OnDiscoveredTagListener tagDiscoveredListener,
                                    boolean handleUnavailableNfc,
                                    boolean disableSounds,
                                    boolean dispatchOnUiThread,
                                    boolean broadcomWorkaround,
                                    boolean noReaderMode) {
        return new TagDispatcher(activity, tagDiscoveredListener, handleUnavailableNfc, disableSounds,
                                 dispatchOnUiThread, broadcomWorkaround, noReaderMode, false);
    }

    /**
    * @deprecated  As of Nordpol 0.2, replaced by
    *              {@link TagDispatcherBuilder}.
    *              Will be removed in v0.3.0 or v1.0.0 (whichever comes first)
    *
    * @param activity               The Activity to attach the TagDispatcher to
    * @param tagDiscoveredListener  The interface for getting new tags
    * @param handleUnavailableNfc   If NFC is unavailable should Nordpol prompt
    *                               the user?
    * @param disableSounds          Should NFC interactions not produce sounds?
    * @param dispatchOnUiThread     Should tags be dispatched on the UI thread?
    * @param broadcomWorkaround     Should the Broadcom workaround be used?
    * @return                       A new TagDispatcher
    */
    public static TagDispatcher get(Activity activity,
                                    OnDiscoveredTagListener tagDiscoveredListener,
                                    boolean handleUnavailableNfc,
                                    boolean disableSounds,
                                    boolean dispatchOnUiThread,
                                    boolean broadcomWorkaround) {
        return new TagDispatcher(activity, tagDiscoveredListener, handleUnavailableNfc, disableSounds,
                                 dispatchOnUiThread, broadcomWorkaround, false, false);
    }

    /**
    * @deprecated  As of Nordpol 0.2, replaced by
    *              {@link TagDispatcherBuilder}.
    *              Will be removed in v0.3.0 or v1.0.0 (whichever comes first)
    *
    * @param activity               The Activity to attach the TagDispatcher to
    * @param tagDiscoveredListener  The interface for getting new tags
    * @param handleUnavailableNfc   If NFC is unavailable should Nordpol prompt
    *                               the user?
    * @param disableSounds          Should NFC interactions not produce sounds?
    * @param dispatchOnUiThread     Should tags be dispatched on the UI thread?
    * @return                       A new TagDispatcher
    */
    public static TagDispatcher get(Activity activity,
                                    OnDiscoveredTagListener tagDiscoveredListener,
                                    boolean handleUnavailableNfc,
                                    boolean disableSounds,
                                    boolean dispatchOnUiThread) {
        return new TagDispatcher(activity, tagDiscoveredListener, handleUnavailableNfc, disableSounds,
                                 dispatchOnUiThread, true, false, false);
    }

    /**
    * @deprecated  As of Nordpol 0.2, replaced by
    *              {@link TagDispatcherBuilder}.
    *              Will be removed in v0.3.0 or v1.0.0 (whichever comes first)
    *
    * @param activity               The Activity to attach the TagDispatcher to
    * @param tagDiscoveredListener  The interface for getting new tags
    * @param handleUnavailableNfc   If NFC is unavailable should Nordpol prompt
    *                               the user?
    * @param disableSounds          Should NFC interactions not produce sounds?
    * @return                       A new TagDispatcher
    */
    public static TagDispatcher get(Activity activity,
                                    OnDiscoveredTagListener tagDiscoveredListener,
                                    boolean handleUnavailableNfc,
                                    boolean disableSounds) {
        return new TagDispatcher(activity, tagDiscoveredListener, handleUnavailableNfc, disableSounds,
                                 true, true, false, false);
    }

    /**
    * @deprecated  As of Nordpol 0.2, replaced by
    *              {@link TagDispatcherBuilder}.
    *              Will be removed in v0.3.0 or v1.0.0 (whichever comes first)
    *
    * @param activity               The Activity to attach the TagDispatcher to
    * @param tagDiscoveredListener  The interface for getting new tags
    * @param handleUnavailableNfc   If NFC is unavailable should Nordpol prompt
    *                               the user?
    * @return                       A new TagDispatcher
    */
    public static TagDispatcher get(Activity activity,
                                    OnDiscoveredTagListener tagDiscoveredListener,
                                    boolean handleUnavailableNfc) {
        return new TagDispatcher(activity, tagDiscoveredListener, handleUnavailableNfc, false,
                                 true, true, false, false);
    }

    /**
    * @deprecated  As of Nordpol 0.2, replaced by
    *              {@link TagDispatcherBuilder}.
    *              Will be removed in v0.3.0 or v1.0.0 (whichever comes first)
    *
    * @param activity               The Activity to attach the TagDispatcher to
    * @param tagDiscoveredListener  The interface for getting new tags
    * @return                       A new TagDispatcher
    */
    public static TagDispatcher get(Activity activity,
                                    OnDiscoveredTagListener tagDiscoveredListener) {
        return new TagDispatcher(activity, tagDiscoveredListener, true, false,
                                 true, true, false, false);
    }


    /** Enable exclusive NFC access for the given activity.
     * Using this method makes NFC intent filters in the AndroidManifest.xml redundant.
     * @return NfcStatus.AVAILABLE_ENABLED if NFC was available and enabled,
     * NfcStatus.AVAILABLE_DISABLED if NFC was available and disabled and
     * NfcStatus.NOT_AVAILABLE if no NFC is available on the device.
     */
    @TargetApi(Build.VERSION_CODES.KITKAT)
    public NfcStatus enableExclusiveNfc() {
        NfcAdapter adapter = NfcAdapter.getDefaultAdapter(activity);
        if (adapter != null) {
            if (!adapter.isEnabled()) {
                if (handleUnavailableNfc) {
                    toastMessage("Please activate NFC and then press back");
                    activity.startActivity(new Intent(android.provider.Settings.ACTION_NFC_SETTINGS));
                }
                return NfcStatus.AVAILABLE_DISABLED;
            }
            if (!noReaderMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                enableReaderMode(adapter);
            } else {
                enableForegroundDispatch(adapter);
            }
            return NfcStatus.AVAILABLE_ENABLED;
        }
        if (handleUnavailableNfc) toastMessage("NFC is not available on this device");
        return NfcStatus.NOT_AVAILABLE;
    }

    /**
     * Disable exclusive NFC access for the given activity.
     */
    @TargetApi(Build.VERSION_CODES.KITKAT)
    public void disableExclusiveNfc() {
        NfcAdapter adapter = NfcAdapter.getDefaultAdapter(activity);
        if (adapter != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                disableReaderMode(adapter);
            } else {
                disableForegroundDispatch(adapter);
            }
        }
    }

    /** Call the TagDispatcher's listener.
     * This applies only to older Android versions (pre-KITKAT) and must
     * be called from onNewIntent(...) in the TagDispatcher's activity.
     *
     * @see <a href="http://developer.android.com/reference/android/app/Activity.html#onNewIntent%28android.content.Intent%29">Activity#onNewIntent</a>
     * @param intent The intent received by onNewIntent
     * @return true if a tag was discovered.
     */
    public boolean interceptIntent(Intent intent) {
        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        if(tag != null) {
            dispatchTag(tag);
            return true;
        } else {
            return false;
        }
    }

    private void dispatchTag(final Tag tag) {
        if(dispatchOnUiThread) {
            if(Looper.myLooper() != Looper.getMainLooper()) {
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            tagDiscoveredListener.tagDiscovered(tag);
                        }
                    });
            } else {
                tagDiscoveredListener.tagDiscovered(tag);
            }

        } else {
            new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... aParams) {
                    tagDiscoveredListener.tagDiscovered(tag);
                    return null;
                }
            }.execute();
        }
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    private void enableReaderMode(NfcAdapter adapter) {
        Bundle options = new Bundle();
        if(broadcomWorkaround) {
            /* This is a work around for some Broadcom chipsets that does
             * the presence check by sending commands that interrupt the
             * processing of the ongoing command.
             */
            options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, DELAY_PRESENCE);
        }
        NfcAdapter.ReaderCallback callback = new NfcAdapter.ReaderCallback() {
                public void onTagDiscovered(Tag tag) {
                    dispatchTag(tag);
                }
            };
        int flags = NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B;
        if(disableSounds) {
            flags = flags | NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS;
        }
        if(disableNdefCheck) {
            flags = flags | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK;
        }
        adapter.enableReaderMode(activity, callback, flags, options);
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    private void disableReaderMode(NfcAdapter adapter) {
        adapter.disableReaderMode(activity);
    }

    private void enableForegroundDispatch(NfcAdapter adapter) {
        /* activity.getIntent() can not be used due to issues with
         * pending intents containing extras of custom classes
         * (https://code.google.com/p/android/issues/detail?id=6822)
         */
        Intent intent = new Intent(activity, activity.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        enableForegroundDispatch(adapter, intent);
    }

    private void enableForegroundDispatch(NfcAdapter adapter, Intent intent) {
        if(adapter.isEnabled()) {
            PendingIntent tagIntent = PendingIntent.getActivity(activity, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
            IntentFilter tag = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
            adapter.enableForegroundDispatch(activity, tagIntent, new IntentFilter[]{tag},
                                             new String[][]{new String[]{IsoDep.class.getName()}});
        }
    }

    private void disableForegroundDispatch(NfcAdapter adapter) {
        adapter.disableForegroundDispatch(activity);
    }

    private void toastMessage(String message){
      Toast.makeText(activity.getApplicationContext(), message, Toast.LENGTH_SHORT).show();
    }
}