package com.example.watch.ui.activity;

import android.Manifest;
import android.app.Dialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;

import com.android.chileaf.WearManager;
import com.android.chileaf.bluetooth.scanner.ScanCallback;
import com.android.chileaf.bluetooth.scanner.ScanRecord;
import com.android.chileaf.bluetooth.scanner.ScanResult;
import com.example.watch.R;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import timber.log.Timber;


/**
 * ScannerFragment class scan required BLE devices and shows them in a list. This class scans and filter
 * devices with standard BLE Service UUID and devices with custom BLE Service UUID. It contains a
 * list and a button to scan/cancel. There is a interface {@link OnDeviceSelectedListener} which is
 * implemented by activity in order to receive selected device. The scanning will continue to scan
 * for 5 seconds and then stop.
 */
public class ScannerFragment extends DialogFragment {

    private static final long SCAN_DURATION = 5000;
    private static final int REQUEST_PERMISSION_REQ_CODE = 34;
    private static final String[] FILTER_NAMES = new String[]{"CL831", "SE2"};

    private BluetoothAdapter mBluetoothAdapter;
    private OnDeviceSelectedListener mListener;
    private DeviceListAdapter mAdapter;
    private final Handler mHandler = new Handler();

    private Button mScanButton;

    private View mPermissionRationale;

    private boolean mIsScanning = false;

    public static ScannerFragment getInstance() {
        final ScannerFragment fragment = new ScannerFragment();
        final Bundle args = new Bundle();
        fragment.setArguments(args);
        return fragment;
    }

    /**
     * Interface required to be implemented by activity.
     */
    public interface OnDeviceSelectedListener {
        /**
         * Fired when user selected the device.
         *
         * @param device the device to connect to
         * @param name   the device name. Unfortunately on some devices {@link BluetoothDevice#getName()}
         *               always returns <code>null</code>, i.e. Sony Xperia Z1 (C6903) with Android 4.3.
         *               The name has to be parsed manually form the Advertisement packet.
         */
        void onDeviceSelected(final BluetoothDevice device, final String name);

        /**
         * Fired when scanner dialog has been cancelled without selecting a device.
         */
        default void onDialogCanceled() {
        }
    }

    /**
     * This will make sure that {@link OnDeviceSelectedListener} interface is implemented by activity.
     */
    @Override
    public void onAttach(final Context context) {
        super.onAttach(context);
        try {
            this.mListener = (OnDeviceSelectedListener) context;
        } catch (final ClassCastException e) {
            throw new ClassCastException(context.toString() + " must implement OnDeviceSelectedListener");
        }
    }

    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final BluetoothManager manager = (BluetoothManager) requireContext().getSystemService(Context.BLUETOOTH_SERVICE);
        if (manager != null) {
            mBluetoothAdapter = manager.getAdapter();
        }
    }

    @Override
    public void onDestroyView() {
        stopScan();
        super.onDestroyView();
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(final Bundle savedInstanceState) {
        final AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
        final View dialogView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_device_scan, null);
        final ListView listview = dialogView.findViewById(android.R.id.list);

        listview.setEmptyView(dialogView.findViewById(android.R.id.empty));
        listview.setAdapter(mAdapter = new DeviceListAdapter(getActivity()));

        builder.setTitle(R.string.scanner_title);
        final AlertDialog dialog = builder.setView(dialogView).create();
        listview.setOnItemClickListener((parent, view, position, id) -> {
            stopScan();
            dialog.dismiss();
            final ExtendedBluetoothDevice d = (ExtendedBluetoothDevice) mAdapter.getItem(position);
            mListener.onDeviceSelected(d.device, d.name);
        });

        mPermissionRationale = dialogView.findViewById(R.id.permission_rationale); // this is not null only on API23+

        mScanButton = dialogView.findViewById(R.id.action_cancel);
        mScanButton.setOnClickListener(v -> {
            if (v.getId() == R.id.action_cancel) {
                if (mIsScanning) {
                    dialog.cancel();
                } else {
                    startScan();
                }
            }
        });

        addBoundDevices();
        if (savedInstanceState == null)
            startScan();
        return dialog;
    }

    @Override
    public void onCancel(DialogInterface dialog) {
        super.onCancel(dialog);
        mListener.onDialogCanceled();
    }

    @Override
    public void onRequestPermissionsResult(final int requestCode, final @NonNull String[] permissions, final @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_PERMISSION_REQ_CODE: {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    // We have been granted the Manifest.permission.ACCESS_COARSE_LOCATION permission. Now we may proceed with scanning.
                    startScan();
                } else {
                    mPermissionRationale.setVisibility(View.VISIBLE);
                    Toast.makeText(getActivity(), R.string.no_required_permission, Toast.LENGTH_SHORT).show();
                }
                break;
            }
        }
    }

    /**
     * Scan for 5 seconds and then stop scanning when a BluetoothLE device is found then mLEScanCallback
     * is activated This will perform regular scan for custom BLE Service UUID and then filter out.
     * using class ScannerServiceParser
     */
    private void startScan() {
        // Since Android 6.0 we need to obtain either Manifest.permission.ACCESS_COARSE_LOCATION or Manifest.permission.ACCESS_FINE_LOCATION to be able to scan for
        // Bluetooth LE devices. This is related to beacons as proximity devices.
        // On API older than Marshmallow the following code does nothing.
        if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            // When user pressed Deny and still wants to use this functionality, show the rationale
            if (ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), Manifest.permission.ACCESS_COARSE_LOCATION) && mPermissionRationale.getVisibility() == View.GONE) {
                mPermissionRationale.setVisibility(View.VISIBLE);
                return;
            }

            requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_PERMISSION_REQ_CODE);
            return;
        }

        // Hide the rationale message, we don't need it anymore.
        if (mPermissionRationale != null)
            mPermissionRationale.setVisibility(View.GONE);

        mAdapter.clearDevices();
        mScanButton.setText(R.string.scanner_action_cancel);

        WearManager.getInstance(getActivity()).startScan(scanCallback);

        mIsScanning = true;
        mHandler.postDelayed(() -> {
            if (mIsScanning) {
                stopScan();
            }
        }, SCAN_DURATION);
    }

    /**
     * Stop scan if user tap Cancel button
     */
    private void stopScan() {
        if (mIsScanning) {
            mScanButton.setText(R.string.scanner_action_scan);
            WearManager.getInstance(getActivity()).stopScan();
            mIsScanning = false;
        }
    }

    private ScanCallback scanCallback = new ScanCallback() {
        @Override
        public void onBatchScanResults(final List<ScanResult> results) {
            mAdapter.update(results);
        }
    };

    private void addBoundDevices() {
        final Set<BluetoothDevice> devices = mBluetoothAdapter.getBondedDevices();
        mAdapter.addBondedDevices(devices);
    }

    private static class DeviceListAdapter extends BaseAdapter {
        private static final int TYPE_TITLE = 0;
        private static final int TYPE_ITEM = 1;
        private static final int TYPE_EMPTY = 2;

        private final ArrayList<ExtendedBluetoothDevice> mListBondedValues = new ArrayList<>();
        private final ArrayList<ExtendedBluetoothDevice> mListValues = new ArrayList<>();
        private final Context mContext;

        public DeviceListAdapter(final Context context) {
            mContext = context;
        }

        /**
         * Sets a list of bonded devices.
         *
         * @param devices list of bonded devices.
         */
        private void addBondedDevices(final Set<BluetoothDevice> devices) {
            final List<ExtendedBluetoothDevice> bondedDevices = mListBondedValues;
            for (BluetoothDevice device : devices) {
                if (matchDeviceName(device.getName())) {
                    bondedDevices.add(new ExtendedBluetoothDevice(device));
                }
            }
            notifyDataSetChanged();
        }

        private boolean matchDeviceName(String name) {
            if (name != null && !TextUtils.isEmpty(name)) {
                for (String filterName : FILTER_NAMES) {
                    if (name.toUpperCase().startsWith(filterName)) {
                        return true;
                    }
                }
            }
            return false;
        }

        /**
         * Updates the list of not bonded devices.
         *
         * @param results list of results from the scanner
         */
        private void update(final List<ScanResult> results) {
            for (final ScanResult result : results) {
                Timber.e(result.toString());
                final ExtendedBluetoothDevice device = findDevice(result);
                if (device == null) {
                    if (hasPairAddress(result)) {
                        mListValues.add(new ExtendedBluetoothDevice(result));
                    }
                } else if (result.getScanRecord() != null) {
                    device.name = result.getScanRecord().getDeviceName();
                    device.rssi = result.getRssi();
                }
            }
            notifyDataSetChanged();
        }

        private boolean hasPairAddress(ScanResult result) {
            ScanRecord record = result.getScanRecord();
            String name = record != null ? record.getDeviceName() : "";
            return matchDeviceName(name);
        }

        private ExtendedBluetoothDevice findDevice(final ScanResult result) {
            for (final ExtendedBluetoothDevice device : mListBondedValues)
                if (device.matches(result) && hasPairAddress(result))
                    return device;
            for (final ExtendedBluetoothDevice device : mListValues)
                if (device.matches(result))
                    return device;
            return null;
        }

        private void clearDevices() {
            mListValues.clear();
            notifyDataSetChanged();
        }

        @Override
        public int getCount() {
            final int bondedCount = mListBondedValues.size() + 1; // 1 for the title
            final int availableCount = mListValues.isEmpty() ? 2 : mListValues.size() + 1; // 1 for title, 1 for empty text
            if (bondedCount == 1)
                return availableCount;
            return bondedCount + availableCount;
        }

        @Override
        public Object getItem(int position) {
            final int bondedCount = mListBondedValues.size() + 1; // 1 for the title
            if (mListBondedValues.isEmpty()) {
                if (position == 0)
                    return R.string.scanner_subtitle_not_bonded;
                else
                    return mListValues.get(position - 1);
            } else {
                if (position == 0)
                    return R.string.scanner_subtitle_bonded;
                if (position < bondedCount)
                    return mListBondedValues.get(position - 1);
                if (position == bondedCount)
                    return R.string.scanner_subtitle_not_bonded;
                return mListValues.get(position - bondedCount - 1);
            }
        }

        @Override
        public int getViewTypeCount() {
            return 3;
        }

        @Override
        public boolean areAllItemsEnabled() {
            return false;
        }

        @Override
        public boolean isEnabled(int position) {
            return getItemViewType(position) == TYPE_ITEM;
        }

        @Override
        public int getItemViewType(int position) {
            if (position == 0)
                return TYPE_TITLE;

            if (!mListBondedValues.isEmpty() && position == mListBondedValues.size() + 1)
                return TYPE_TITLE;

            if (position == getCount() - 1 && mListValues.isEmpty())
                return TYPE_EMPTY;

            return TYPE_ITEM;
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View oldView, ViewGroup parent) {
            final LayoutInflater inflater = LayoutInflater.from(mContext);
            final int type = getItemViewType(position);

            View view = oldView;
            switch (type) {
                case TYPE_EMPTY:
                    if (view == null) {
                        view = new TextView(mContext);
                        final TextView empty = (TextView) view;
                        empty.setGravity(Gravity.CENTER_HORIZONTAL);
                        empty.setText(mContext.getString(R.string.scanner_empty));
                    }
                    break;
                case TYPE_TITLE:
                    if (view == null) {
                        view = new TextView(mContext);
                    }
                    final TextView title = (TextView) view;
                    title.setGravity(Gravity.CENTER_HORIZONTAL);
                    title.setText((Integer) getItem(position));
                    break;
                default:
                    if (view == null) {
                        view = inflater.inflate(R.layout.item_device_list, parent, false);
                        final ViewHolder holder = new ViewHolder();
                        holder.name = view.findViewById(R.id.name);
                        holder.address = view.findViewById(R.id.address);
                        holder.signal = view.findViewById(R.id.rssi);
                        view.setTag(holder);
                    }

                    final ExtendedBluetoothDevice device = (ExtendedBluetoothDevice) getItem(position);
                    final ViewHolder holder = (ViewHolder) view.getTag();
                    final String name = device.name;
                    holder.name.setText(name != null ? name : mContext.getString(R.string.not_available));
                    holder.address.setText(device.device.getAddress());
                    if (!device.isBonded || device.rssi != ExtendedBluetoothDevice.NO_RSSI) {
                        holder.signal.setText(device.rssi + "dBm");
                        holder.signal.setVisibility(View.VISIBLE);
                    } else {
                        holder.signal.setVisibility(View.GONE);
                    }
                    break;
            }
            return view;
        }

        private class ViewHolder {
            private TextView name;
            private TextView address;
            private TextView signal;
        }
    }

    private static class ExtendedBluetoothDevice {

        private static final int NO_RSSI = -1000;

        private String name;
        private int rssi;
        private boolean isBonded;
        private final BluetoothDevice device;

        private ExtendedBluetoothDevice(final ScanResult scanResult) {
            this.device = scanResult.getDevice();
            this.name = scanResult.getScanRecord() != null ? scanResult.getScanRecord().getDeviceName() : null;
            this.rssi = scanResult.getRssi();
            this.isBonded = false;
        }

        private ExtendedBluetoothDevice(final BluetoothDevice device) {
            this.device = device;
            this.name = device.getName();
            this.rssi = NO_RSSI;
            this.isBonded = true;
        }

        private boolean matches(final ScanResult scanResult) {
            return device.getAddress().equals(scanResult.getDevice().getAddress());
        }
    }
}
