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.*
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.ScanResult
import com.example.watch.R
import com.example.watch.ui.activity.ScannerFragment.OnDeviceSelectedListener
import timber.log.Timber
import java.util.*

/**
 * 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 [OnDeviceSelectedListener] which is
 * implemented by activity in order to receive selected device. The scanning will continue to scan
 * for 5 seconds and then stop.
 */
class ScannerFragment : DialogFragment() {
    private var mBluetoothAdapter: BluetoothAdapter? = null
    private var mListener: OnDeviceSelectedListener? = null
    private var mAdapter: DeviceListAdapter? = null
    private val mHandler = Handler()
    private var mScanButton: Button? = null
    private var mPermissionRationale: View? = null
    private var mIsScanning = false
    
    fun getInstance(): ScannerFragment? {
        val fragment = ScannerFragment()
        val args = Bundle()
        fragment.arguments = args
        return fragment
    }

    /**
     * Interface required to be implemented by activity.
     */
    interface OnDeviceSelectedListener {
        /**
         * Fired when user selected the device.
         *
         * @param device the device to connect to
         * @param name   the device name. Unfortunately on some devices [BluetoothDevice.getName]
         * always returns `null`, i.e. Sony Xperia Z1 (C6903) with Android 4.3.
         * The name has to be parsed manually form the Advertisement packet.
         */
        fun onDeviceSelected(device: BluetoothDevice?, name: String?)

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

    /**
     * This will make sure that [OnDeviceSelectedListener] interface is implemented by activity.
     */
    override fun onAttach(context: Context) {
        super.onAttach(context)
        try {
            mListener = context as OnDeviceSelectedListener
        } catch (e: ClassCastException) {
            throw ClassCastException("$context must implement OnDeviceSelectedListener")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val manager =
            requireContext().getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        if (manager != null) {
            mBluetoothAdapter = manager.adapter
        }
    }

    override fun onDestroyView() {
        stopScan()
        super.onDestroyView()
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(requireContext())
        val dialogView: View =
            LayoutInflater.from(activity).inflate(R.layout.fragment_device_scan, null)
        val listview = dialogView.findViewById<ListView>(R.id.list)
        listview.emptyView = dialogView.findViewById(R.id.empty)
        listview.adapter = activity?.let {
            DeviceListAdapter(it)
                .also({ mAdapter = it })
        }
        builder.setTitle(R.string.scanner_title)
        val dialog = builder.setView(dialogView).create()
        listview.onItemClickListener =
            AdapterView.OnItemClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long ->
                stopScan()
                dialog.dismiss()
                val d: ExtendedBluetoothDevice =
                    mAdapter?.getItem(position) as ExtendedBluetoothDevice
                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(View.OnClickListener { v: View ->
            if (v.id == R.id.action_cancel) {
                if (mIsScanning) {
                    dialog.cancel()
                } else {
                    startScan()
                }
            }
        })
        addBoundDevices()
        if (savedInstanceState == null) startScan()
        return dialog
    }

    override fun onCancel(dialog: DialogInterface) {
        super.onCancel(dialog)
            mListener?.onDialogCanceled()
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        when (requestCode) {
            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!!.visibility = View.VISIBLE
                    Toast.makeText(activity, R.string.no, Toast.LENGTH_SHORT)
                        .show()
                }
            }
        }
    }

    /**
     * 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 fun 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!!.visibility == View.GONE
            ) {
                mPermissionRationale!!.visibility = View.VISIBLE
                return
            }
            requestPermissions(
                arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
                REQUEST_PERMISSION_REQ_CODE
            )
            return
        }

        // Hide the rationale message, we don't need it anymore.
        if (mPermissionRationale != null) mPermissionRationale!!.visibility = View.GONE
        mAdapter?.clearDevices()
        mScanButton?.setText(R.string.scanner_action_cancel)
        WearManager.getInstance(activity).startScan(scanCallback)
        mIsScanning = true
        mHandler.postDelayed({
            if (mIsScanning) {
                stopScan()
            }
        }, SCAN_DURATION)
    }

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

    private val scanCallback: ScanCallback = object : ScanCallback() {
        override fun onBatchScanResults(results: List<ScanResult>) {
            mAdapter?.update(results)
        }
    }

    private fun addBoundDevices() {
        val devices = mBluetoothAdapter!!.bondedDevices
        mAdapter?.addBondedDevices(devices)
    }

    private class DeviceListAdapter(private val mContext: Context) :
        BaseAdapter() {
        private val mListBondedValues: ArrayList<com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice> =
            ArrayList<com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice>()
        private val mListValues: ArrayList<com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice> =
            ArrayList<com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice>()

        /**
         * Sets a list of bonded devices.
         *
         * @param devices list of bonded devices.
         */
        fun addBondedDevices(devices: Set<BluetoothDevice>) {
            val bondedDevices: MutableList<com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice> =
                mListBondedValues
            for (device in devices) {
                if (matchDeviceName(device.name)) {
                    bondedDevices.add(
                        com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice(
                            device
                        )
                    )
                }
            }
            notifyDataSetChanged()
        }

        private fun matchDeviceName(name: String?): Boolean {
            if (name != null && !TextUtils.isEmpty(name)) {
                for (filterName in 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
         */
        fun update(results: List<ScanResult>) {
            for (result in results) {
                Timber.e(result.toString())
                val device: com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice? =
                    findDevice(result)
                if (device == null) {
                    if (hasPairAddress(result)) {
                        mListValues.add(
                            com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice(
                                result
                            )
                        )
                    }
                } else if (result.scanRecord != null) {
                    device.name = result.scanRecord!!.deviceName
                    device.rssi = result.rssi
                }
            }
            notifyDataSetChanged()
        }

        private fun hasPairAddress(result: ScanResult): Boolean {
            val record = result.scanRecord
            val name = if (record != null) record.deviceName else ""
            return matchDeviceName(name)
        }

        private fun findDevice(result: ScanResult): com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice? {
            for (device in mListBondedValues) if (device.matches(result) && hasPairAddress(result)) return device
            for (device in mListValues) if (device.matches(result)) return device
            return null
        }

        fun clearDevices() {
            mListValues.clear()
            notifyDataSetChanged()
        }

        override fun getCount(): Int {
            val bondedCount = mListBondedValues.size + 1 // 1 for the title
            val availableCount =
                if (mListValues.isEmpty()) 2 else mListValues.size + 1 // 1 for title, 1 for empty text
            return if (bondedCount == 1) availableCount else bondedCount + availableCount
        }

        override fun getItem(position: Int): Any {
            val bondedCount = mListBondedValues.size + 1 // 1 for the title
            return if (mListBondedValues.isEmpty()) {
                if (position == 0) R.string.scanner_subtitle_not_bonded else mListValues[position - 1]
            } else {
                if (position == 0) return R.string.scanner_subtitle_bonded
                if (position < bondedCount) return mListBondedValues[position - 1]
                if (position == bondedCount) R.string.scanner_subtitle_not_bonded else mListValues[position - bondedCount - 1]
            }
        }

        override fun getViewTypeCount(): Int {
            return 3
        }

        override fun areAllItemsEnabled(): Boolean {
            return false
        }

        override fun isEnabled(position: Int): Boolean {
            return getItemViewType(position) == com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_ITEM
        }

        override fun getItemViewType(position: Int): Int {
            if (position == 0) return com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_TITLE
            if (!mListBondedValues.isEmpty() && position == mListBondedValues.size + 1) return com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_TITLE
            return if (position == count - 1 && mListValues.isEmpty()) com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_EMPTY else com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_ITEM
        }

        override fun getItemId(position: Int): Long {
            return position.toLong()
        }

        override fun getView(position: Int, oldView: View, parent: ViewGroup): View {
            val inflater = LayoutInflater.from(mContext)
            val type = getItemViewType(position)
            var view = oldView
            when (type) {
                com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_EMPTY -> if (view == null) {
                    view = TextView(mContext)
                    val empty = view
                    empty.gravity = Gravity.CENTER_HORIZONTAL
                    empty.text = mContext.getString(R.string.scanner_empty)
                }
                com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_TITLE -> {
                    if (view == null) {
                        view = TextView(mContext)
                    }
                    val title = view as TextView
                    title.gravity = Gravity.CENTER_HORIZONTAL
                    title.setText((getItem(position) as Int))
                }
                else -> {
//                    if (view == null) {
//                        view = inflater.inflate(R.layout.item_device_list, parent, false)
//                        val holder: com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.ViewHolder =
//                            com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.ViewHolder()
//
//                        holder.name = view.findViewById<TextView>(R.id.name)
//                        holder.address = view.findViewById<TextView>(R.id.address)
//                        holder.signal = view.findViewById<TextView>(R.id.rssi)
//                        view.tag = holder
//                    }
                    val device: com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice =
                        getItem(position) as com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice
                    val holder: com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.ViewHolder =
                        view.tag as com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.ViewHolder
                    val name: String? = device.name
                    holder.name?.setText(name ?: mContext.getString(R.string.not_available))
                    holder.address?.setText(device.device.getAddress())
                    if (!device.isBonded || device.rssi != com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice.Companion.NO_RSSI) {
                        holder.signal?.setText(device.rssi.toString() + "dBm")
                        holder.signal?.setVisibility(View.VISIBLE)
                    } else {
                        holder.signal?.setVisibility(View.GONE)
                    }
                }
            }
            return view
        }

        private inner class ViewHolder {
            var name: TextView? = null
            var address: TextView? = null
            var signal: TextView? = null
        }

        companion object {
            private const val TYPE_TITLE = 0
            private const val TYPE_ITEM = 1
            private const val TYPE_EMPTY = 2
        }
    }

    private class ExtendedBluetoothDevice {
        var name: String?
        var rssi: Int
        var isBonded: Boolean
        val device: BluetoothDevice

        constructor(scanResult: ScanResult) {
            device = scanResult.device
            name = if (scanResult.scanRecord != null) scanResult.scanRecord!!.deviceName else null
            rssi = scanResult.rssi
            isBonded = false
        }

        constructor(device: BluetoothDevice) {
            this.device = device
            name = device.name
            rssi =
                com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice.Companion.NO_RSSI
            isBonded = true
        }

        fun matches(scanResult: ScanResult): Boolean {
            return device.address == scanResult.device.address
        }

        companion object {
            const val NO_RSSI = -1000
        }
    }

    companion object {
        @JvmName("getInstance1")
        fun getInstance(): ScannerFragment {
            val fragment = ScannerFragment()
            val args = Bundle()
            fragment.arguments = args
            return fragment
        }

        private const val SCAN_DURATION: Long = 5000
        private const val REQUEST_PERMISSION_REQ_CODE = 34
        private val FILTER_NAMES = arrayOf("CL831", "SE2")
        val instance: ScannerFragment
            get() {
                val fragment = ScannerFragment()
                val args = Bundle()
                fragment.arguments = args
                return fragment
            }
    }
}