/*
 * Copyright (c) 2025 Meshtastic LLC
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.geeksville.mesh.android

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.IInterface
import com.geeksville.mesh.util.exceptionReporter
import java.io.Closeable
import java.lang.IllegalArgumentException
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

class BindFailedException : Exception("bindService failed")

/**
 * A wrapper that cleans up the service binding process
 */
open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T) : Closeable,
    Logging {

    var serviceP: T? = null

    // A getter that returns the bound service or throws if not bound
    val service: T
        get() {
            waitConnect() // Wait for at least the initial connection to happen
            return serviceP ?: throw Exception("Service not bound")
        }

    private var context: Context? = null

    private var isClosed = true

    private val lock = ReentrantLock()
    private val condition = lock.newCondition()

    /** Call this if you want to stall until the connection is completed */
    fun waitConnect() {
        // Wait until this service is connected
        lock.withLock {
            if (context == null) {
                throw Exception("Haven't called connect")
            }

            if (serviceP == null) {
                condition.await()
            }
        }
    }

    fun connect(c: Context, intent: Intent, flags: Int) {
        context = c
        if (isClosed) {
            isClosed = false
            if (!c.bindService(intent, connection, flags)) {

                // Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false.  Try
                // a short sleep to see if that helps
                errormsg("Needed to use the second bind attempt hack")
                Thread.sleep(500) // was 200ms, but received an autobug from a Galaxy Note4, android 6.0.1
                if (!c.bindService(intent, connection, flags)) {
                    throw BindFailedException()
                }
            }
        } else {
            warn("Ignoring rebind attempt for service")
        }
    }

    override fun close() {
        isClosed = true
        try {
            context?.unbindService(connection)
        } catch (ex: IllegalArgumentException) {
            // Autobugs show this can generate an illegal arg exception for "service not registered" during reinstall?
            warn("Ignoring error in ServiceClient.close, probably harmless")
        }
        serviceP = null
        context = null
    }

    // Called when we become connected
    open fun onConnected(service: T) {
    }

    // called on loss of connection
    open fun onDisconnected() {
    }

    private val connection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, binder: IBinder) = exceptionReporter {
            if (!isClosed) {
                val s = stubFactory(binder)
                serviceP = s
                onConnected(s)

                // after calling our handler, tell anyone who was waiting for this connection to complete
                lock.withLock {
                    condition.signalAll()
                }
            } else {
                // If we start to close a service, it seems that there is a possibility a onServiceConnected event is the queue
                // for us.  Be careful not to process that stale event
                warn("A service connected while we were closing it, ignoring")
            }
        }

        override fun onServiceDisconnected(name: ComponentName?) = exceptionReporter {
            serviceP = null
            onDisconnected()
        }
    }
}
