/*
 * SPDX-FileCopyrightText: 2020, microG Project Team
 * SPDX-License-Identifier: Apache-2.0
 */

package org.microg.gms.nearby.exposurenotification

import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.AlarmManager
import android.app.PendingIntent.FLAG_ONE_SHOT
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.bluetooth.BluetoothAdapter.*
import android.bluetooth.le.*
import android.bluetooth.le.AdvertiseSettings.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build.VERSION.SDK_INT
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import androidx.core.app.PendingIntentCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.common.ForegroundServiceContext
import org.microg.gms.common.ForegroundServiceInfo
import java.io.FileDescriptor
import java.io.PrintWriter
import java.nio.ByteBuffer
import java.util.*

@TargetApi(21)
@ForegroundServiceInfo("Exposure Notification")
class AdvertiserService : LifecycleService() {
    private val version = VERSION_1_1
    private var advertising = false
    private var wantStartAdvertising = false
    private val advertiser: BluetoothLeAdvertiser?
        get() = getDefaultAdapter()?.bluetoothLeAdvertiser
    private val alarmManager: AlarmManager
        get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
    private val callback: AdvertiseCallback = object : AdvertiseCallback() {
        override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
            Log.d(TAG, "Advertising active for ${settingsInEffect?.timeout}ms")
        }

        override fun onStartFailure(errorCode: Int) {
            Log.w(TAG, "Advertising failed: $errorCode")
            stopOrRestartAdvertising()
        }
    }

    @TargetApi(23)
    private var setCallback: Any? = null
    private val trigger = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            if (intent?.action == ACTION_STATE_CHANGED) {
                when (intent.getIntExtra(EXTRA_STATE, -1)) {
                    STATE_TURNING_OFF, STATE_OFF -> stopOrRestartAdvertising()
                    STATE_ON -> startAdvertisingIfNeeded()
                }
            }
        }
    }
    private val handler = Handler(Looper.getMainLooper())
    private val startLaterRunnable = Runnable { startAdvertisingIfNeeded() }

    override fun onCreate() {
        super.onCreate()
        registerReceiver(trigger, IntentFilter().apply { addAction(ACTION_STATE_CHANGED) })
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        ForegroundServiceContext.completeForegroundService(this, intent, TAG)
        Log.d(TAG, "AdvertisingService.start: $intent")
        super.onStartCommand(intent, flags, startId)
        if (intent?.action == ACTION_RESTART_ADVERTISING && advertising) {
            stopOrRestartAdvertising()
        } else {
            startAdvertisingIfNeeded()
        }
        return START_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(trigger)
        stopOrRestartAdvertising()
        handler.removeCallbacks(startLaterRunnable)
    }

    private fun startAdvertisingIfNeeded() {
        if (ExposurePreferences(this).enabled) {
            lifecycleScope.launchWhenStarted {
                withContext(Dispatchers.IO) {
                    startAdvertising()
                }
            }
        } else {
            stopSelf()
        }
    }

    private var lastStartTime = System.currentTimeMillis()
    private var sendingBytes = ByteArray(0)
    private var starting = false

    @SuppressLint("MissingPermission")
    private suspend fun startAdvertising() {
        val advertiser = synchronized(this) {
            if (advertising || starting) return
            val advertiser = advertiser ?: return
            wantStartAdvertising = false
            starting = true
            advertiser
        }
        try {
            val aemBytes = when (version) {
                VERSION_1_0 -> byteArrayOf(
                        version, // Version and flags
                        currentDeviceInfo.txPowerCorrection, // TX power
                        0x00, // Reserved
                        0x00  // Reserved
                )
                VERSION_1_1 -> byteArrayOf(
                        (version + currentDeviceInfo.confidence.toByte() * 4).toByte(), // Version and flags
                        currentDeviceInfo.txPowerCorrection, // TX power
                        0x00, // Reserved
                        0x00  // Reserved
                )
                else -> return
            }
            var nextSend = nextKeyMillis.coerceAtLeast(10000)
            val payload = ExposureDatabase.with(this@AdvertiserService) { database ->
                database.generateCurrentPayload(aemBytes)
            }
            val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, payload).build()
            Log.i(TAG, "Starting advertiser")
            if (SDK_INT >= 26) {
                setCallback = SetCallback()
                val params = AdvertisingSetParameters.Builder()
                        .setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM)
                        .setLegacyMode(true)
                        .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_LOW)
                        .setConnectable(false)
                        .build()
                try {
                    advertiser.startAdvertisingSet(params, data, null, null, null, setCallback as AdvertisingSetCallback)
                } catch (e: SecurityException) {
                    Log.e(TAG, "Couldn't start advertising: Need android.permission.BLUETOOTH_ADVERTISE permission.", )
                }
            } else {
                nextSend = nextSend.coerceAtMost(180000)
                val settings = Builder()
                        .setTimeout(nextSend.toInt())
                        .setAdvertiseMode(ADVERTISE_MODE_BALANCED)
                        .setTxPowerLevel(ADVERTISE_TX_POWER_LOW)
                        .setConnectable(false)
                        .build()
                try {
                    advertiser.startAdvertising(settings, data, callback)
                } catch (e: SecurityException) {
                    Log.e(TAG, "Couldn't start advertising.", )
                }
            }
            synchronized(this) { advertising = true }
            sendingBytes = payload
            lastStartTime = System.currentTimeMillis()
            scheduleRestartAdvertising(nextSend)
        } finally {
            synchronized(this) { starting = false }
        }
    }

    override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
        writer?.println("Advertising: $advertising")
        try {
            val startTime = lastStartTime
            val bytes = sendingBytes
            val (uuid, aem) = ByteBuffer.wrap(bytes).let { UUID(it.long, it.long) to it.int }
            writer?.println("""
                Last advertising:
                    Since: ${Date(startTime)}
                    RPI: $uuid
                    Version: 0x${version.toString(16)}
                    TX Power: ${currentDeviceInfo.txPowerCorrection}
                    AEM: 0x${aem.toLong().let { if (it < 0) 0x100000000L + it else it }.toString(16)}
                """.trimIndent())
        } catch (e: Exception) {
            writer?.println("Last advertising: ${e.message ?: e.toString()}")
        }
    }

    private fun scheduleRestartAdvertising(nextSend: Long) {
        val intent = Intent(this, AdvertiserService::class.java).apply { action = ACTION_RESTART_ADVERTISING }
        val pendingIntent = PendingIntentCompat.getService(this, ACTION_RESTART_ADVERTISING.hashCode(), intent, FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT, false)!!
        when {
            SDK_INT >= 23 ->
                alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextSend, pendingIntent)
            else ->
                alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextSend, pendingIntent)
        }
    }

    @Synchronized
    @SuppressLint("MissingPermission")
    private fun stopOrRestartAdvertising() {
        if (!advertising) return
        Log.i(TAG, "Stopping advertiser")
        advertising = false
        if (SDK_INT >= 26) {
            wantStartAdvertising = true
            try {
                advertiser?.stopAdvertisingSet(setCallback as AdvertisingSetCallback)
            } catch (e: SecurityException) {
                Log.i(TAG, "Tried calling stopAdvertisingSet without android.permission.BLUETOOTH_ADVERTISE permission.", )
            }
        } else {
            try {
                advertiser?.stopAdvertising(callback)
            } catch (e: SecurityException) {
                Log.i(TAG, "stopAdvertising() failed with a SecurityException. Maybe some permissions are missing?", )
            }
        }
        handler.postDelayed(startLaterRunnable, 1000)
    }

    @TargetApi(26)
    inner class SetCallback : AdvertisingSetCallback() {
        override fun onAdvertisingSetStarted(advertisingSet: AdvertisingSet?, txPower: Int, status: Int) {
            Log.d(TAG, "Advertising active, status=$status")
        }

        override fun onAdvertisingSetStopped(advertisingSet: AdvertisingSet?) {
            Log.d(TAG, "Advertising stopped")
            if (wantStartAdvertising) {
                startAdvertisingIfNeeded()
            } else {
                stopOrRestartAdvertising()
            }
        }
    }


    companion object {
        private const val ACTION_RESTART_ADVERTISING = "org.microg.gms.nearby.exposurenotification.RESTART_ADVERTISING"

        fun isNeeded(context: Context): Boolean {
            return ExposurePreferences(context).enabled
        }

        fun isSupported(context: Context): Boolean? {
            val adapter = getDefaultAdapter()
            return when {
                adapter == null -> false
                SDK_INT >= 26 && (adapter.isLeExtendedAdvertisingSupported || adapter.isLePeriodicAdvertisingSupported) -> true
                adapter.state != STATE_ON -> null
                adapter.bluetoothLeAdvertiser != null -> true
                else -> false
            }
        }
    }
}
