package jp.juggler.subwaytooter

import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.os.Handler
import android.util.Log
import androidx.emoji2.bundled.BundledEmojiCompatConfig
import androidx.emoji2.text.EmojiCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.engine.executor.GlideExecutor
import com.bumptech.glide.load.model.GlideUrl
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.emoji.EmojiMap
import jp.juggler.subwaytooter.pref.LazyContextHolder
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.CustomEmojiCache
import jp.juggler.subwaytooter.util.CustomEmojiLister
import jp.juggler.subwaytooter.util.ProgressResponseBody
import jp.juggler.subwaytooter.util.getUserAgent
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.initializeToastUtils
import jp.juggler.util.network.MySslSocketFactory
import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.os.applicationContextSafe
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.ConnectionSpec
import okhttp3.CookieJar
import okhttp3.Interceptor
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.conscrypt.Conscrypt
import ru.gildor.coroutines.okhttp.await
import java.io.File
import java.io.InputStream
import java.net.CookieHandler
import java.net.CookieManager
import java.net.CookiePolicy
import java.security.Security
import java.util.Collections
import java.util.concurrent.TimeUnit
import java.util.logging.Level
import java.util.logging.Logger
import kotlin.math.max

class App1 : Application() {

    override fun onCreate() {
        log.d("onCreate")
        LazyContextHolder.init(applicationContextSafe)
        super.onCreate()
        initializeToastUtils(this)
        prepare(applicationContext, "App1.onCreate")
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        LazyContextHolder.init(applicationContextSafe)
    }

    override fun onTerminate() {
        log.d("onTerminate")
        super.onTerminate()
    }

    companion object {

        internal val log = LogCategory("App1")

        //		private val APPROVED_CIPHER_SUITES = arrayOf(
        //
        //			// 以下は okhttp 3 のデフォルト
        //			// This is nearly equal to the cipher suites supported in Chrome 51, current as of 2016-05-25.
        //			// All of these suites are available on Android 7.0; earlier releases support a subset of these
        //			// suites. https://github.com/square/okhttp/issues/1972
        //			CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
        //			CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
        //			CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
        //			CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
        //			CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
        //			CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
        //
        //			// Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll
        //			// continue to include them until better suites are commonly available. For example, none
        //			// of the better cipher suites listed above shipped with Android 4.4 or Java 7.
        //			CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
        //			CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
        //			CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
        //			CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
        //			CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
        //			CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,
        //			CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
        //			CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
        //			CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
        //
        //			//https://www.ssllabs.com/ssltest/analyze.html?d=mastodon.cloud&latest
        //			CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, // mastodon.cloud用 デフォルトにはない
        //			CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, //mastodon.cloud用 デフォルトにはない
        //
        //			// https://www.ssllabs.com/ssltest/analyze.html?d=m.sighash.info
        //			CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, // m.sighash.info 用 デフォルトにはない
        //			CipherSuite.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, // m.sighash.info 用 デフォルトにはない
        //			CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, // m.sighash.info 用 デフォルトにはない
        //			CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA
        //		) // m.sighash.info 用 デフォルトにはない

        //	private int getBitmapPoolSize( Context context ){
        //		ActivityManager am = ((ActivityManager)context.getSystemService(Activity.ACTIVITY_SERVICE));
        //		int memory = am.getMemoryClass();
        //		int largeMemory = am.getLargeMemoryClass();
        //		// どちらも単位はMB
        //		warning.d("MemoryClass=%d, LargeMemoryClass = %d",memory,largeMemory);
        //
        //		int maxSize;
        //		if( am.isLowRamDevice() ){
        //			maxSize = 5 * 1024; // 単位はKiB
        //		}else if( largeMemory >= 512 ){
        //			maxSize = 128 * 1024; // 単位はKiB
        //		}else if( largeMemory >= 256 ){
        //			maxSize = 64 * 1024; // 単位はKiB
        //		}else{
        //			maxSize = 10 * 1024; // 単位はKiB
        //		}
        //		return maxSize * 1024;
        //	}

        private var cookieManager: CookieManager? = null
        private var cookieJar: CookieJar? = null

        private fun Context.userAgentInterceptor() =
            Interceptor { chain ->
                chain.proceed(
                    chain.request().newBuilder()
                        .header("User-Agent", getUserAgent())
                        .build()
                )
            }

        private fun Context.prepareOkHttp(
            timeoutSecondsConnect: Int,
            timeoutSecondsRead: Int,
        ): OkHttpClient.Builder {

            Logger.getLogger(OkHttpClient::class.java.name).level = Level.FINE

            var cookieJar = this@Companion.cookieJar
            if (cookieJar == null) {
                val cookieManager = CookieManager().apply {
                    setCookiePolicy(CookiePolicy.ACCEPT_ALL)
                }
                CookieHandler.setDefault(cookieManager)
                cookieJar = JavaNetCookieJar(cookieManager)

                this@Companion.cookieManager = cookieManager
                this@Companion.cookieJar = cookieJar
            }

            val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
                .allEnabledCipherSuites()
                .allEnabledTlsVersions()
                .build()

            return OkHttpClient.Builder()
                .connectTimeout(timeoutSecondsConnect.toLong(), TimeUnit.SECONDS)
                .readTimeout(timeoutSecondsRead.toLong(), TimeUnit.SECONDS)
                .writeTimeout(timeoutSecondsRead.toLong(), TimeUnit.SECONDS)
                .pingInterval(10, TimeUnit.SECONDS)
                .connectionSpecs(Collections.singletonList(spec))
                .sslSocketFactory(MySslSocketFactory, MySslSocketFactory.trustManager)
                .addInterceptor(ProgressResponseBody.makeInterceptor())
                .addInterceptor(userAgentInterceptor())

            // クッキーの導入は検討中。とりあえずmstdn.jpではクッキー有効でも改善しなかったので現時点では追加しない
            //	.cookieJar(cookieJar)
        }

        lateinit var ok_http_client: OkHttpClient

        private lateinit var ok_http_client2: OkHttpClient

        lateinit var ok_http_client_media_viewer: OkHttpClient

        // lateinit var task_executor : ThreadPoolExecutor

        @SuppressLint("StaticFieldLeak")
        lateinit var custom_emoji_cache: CustomEmojiCache

        @SuppressLint("StaticFieldLeak")
        lateinit var custom_emoji_lister: CustomEmojiLister

        fun prepare(appContext: Context, caller: String): AppState {
            var state = appStateX
            if (state != null) return state

            log.d("initialize AppState. caller=$caller")

            // initialize EmojiMap
            EmojiMap.load(appContext)

            // emoji2 はデフォルトで自動初期化を行うのだが、新し目のPlayサービスに依存してるため
            // Playサービスが古い端末ではEmojiCompatの初期化がまだ行われていない状態になる
            // ワークアラウンドとして、アプリ内にバンドルしたデータを使うBundledEmojiCompatConfigで初期化する
            // (初期化が既に行われている場合は無害である)
            EmojiCompat.init(BundledEmojiCompatConfig(appContext))

            // initialize Conscrypt
            Security.insertProviderAt(
                Conscrypt.newProvider(),
                1 /* 1 means first position */
            )

            // We want at least 2 threads and at most 4 threads in the core pool,
            // preferring to have 1 less than the CPU count to avoid saturating
            // the CPU with background work

            //				val CPU_COUNT = Runtime.getRuntime().availableProcessors()
            //				val CORE_POOL_SIZE = max(2, min(CPU_COUNT - 1, 4))
            //				val MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1
            //				val KEEP_ALIVE_SECONDS = 30

            //				// デフォルトだとキューはmax128で、溢れることがある
            //				val sPoolWorkQueue = LinkedBlockingQueue<Runnable>(999)
            //
            //				val sThreadFactory = object : ThreadFactory {
            //					private val mCount = AtomicInteger(1)
            //
            //					override fun newThread(r : Runnable) : Thread {
            //						return Thread(r, "SubwayTooterTask #" + mCount.getAndIncrement())
            //					}
            //				}

            //				task_executor = ThreadPoolExecutor(
            //					CORE_POOL_SIZE  // pool size
            //					, MAXIMUM_POOL_SIZE // max pool size
            //					, KEEP_ALIVE_SECONDS.toLong() // keep-alive-seconds
            //					, TimeUnit.SECONDS // unit of keep-alive-seconds
            //					, sPoolWorkQueue, sThreadFactory
            //				)
            //
            //				task_executor.allowCoreThreadTimeOut(true)

            //		if( USE_OLD_EMOJIONE ){
            //			if( typeface_emoji == null ){
            //				typeface_emoji = TypefaceUtils.load( app_context.getAssets(), "emojione_android.ttf" );
            //			}
            //		}

            //		if( image_loader == null ){
            //			image_loader = new MyImageLoader(
            //				Volley.newRequestQueue( getApplicationContext() )
            //				, new BitmapCache( getApplicationContext() )
            //			);
            //		}

            log.d("create okhttp client")
            run {

                Logger.getLogger(OkHttpClient::class.java.name).level = Level.FINE

                val apiReadTimeout = max(3, PrefS.spApiReadTimeout.toInt())

                // API用のHTTP設定はキャッシュを使わない
                ok_http_client = appContext.prepareOkHttp(apiReadTimeout, apiReadTimeout)
                    .build()

                // ディスクキャッシュ
                val cacheDir = File(appContext.cacheDir, "http2")
                val cache = Cache(cacheDir, 30000000L)

                // カスタム絵文字用のHTTP設定はキャッシュを使う
                ok_http_client2 = appContext.prepareOkHttp(apiReadTimeout, apiReadTimeout)
                    .cache(cache)
                    .build()

                // 内蔵メディアビューア用のHTTP設定はタイムアウトを調整可能
                val mediaReadTimeout = max(3, PrefS.spMediaReadTimeout.toInt())
                ok_http_client_media_viewer =
                    appContext.prepareOkHttp(mediaReadTimeout, mediaReadTimeout)
                        .cache(cache)
                        .build()
            }

            val handler = Handler(appContext.mainLooper)

            log.d("create custom emoji cache.")
            custom_emoji_cache = CustomEmojiCache(appContext, handler)
            custom_emoji_lister = CustomEmojiLister(appContext, handler)

            ColumnType.dump()

            log.d("create  AppState.")

            state = AppState(appContext, handler)
            appStateX = state

            // getAppState()を使える状態にしてからカラム一覧をロードする
            log.d("load column list...")
            state.loadColumnList()

            log.d("prepare() complete! caller=$caller")

            return state
        }

        @SuppressLint("StaticFieldLeak")
        private var appStateX: AppState? = null

        fun getAppState(context: Context, caller: String = "getAppState"): AppState {
            return prepare(context.applicationContext, caller)
        }

        fun sound(item: HighlightWord) {
            try {
                appStateX?.sound(item)
            } catch (ex: Throwable) {
                log.e(ex, "sound failed.")
                // java.lang.NoSuchFieldError:
                // at jp.juggler.subwaytooter.App1$Companion.sound (App1.kt:544)
                // at jp.juggler.subwaytooter.column.Column$startRefresh$task$1.onPostExecute (Column.kt:2432)
            }
        }

        @Suppress("UNUSED_PARAMETER")
        fun registerGlideComponents(context: Context, glide: Glide, registry: Registry) {
            // カスタムされたokhttpを優先的に使うためにprependを指定する
            registry.prepend(
                GlideUrl::class.java,
                InputStream::class.java,
                OkHttpUrlLoader.Factory(ok_http_client)
            )
        }

        fun applyGlideOptions(context: Context, builder: GlideBuilder) {

            // ログレベル
            builder.setLogLevel(Log.ERROR)

            // エラー処理
            val catcher = GlideExecutor.UncaughtThrowableStrategy { ex ->
                log.e(ex, "glide uncaught error.")
            }
            builder.setDiskCacheExecutor(
                GlideExecutor.newDiskCacheBuilder()
                    .setUncaughtThrowableStrategy(catcher).build()
            )
            builder.setSourceExecutor(
                GlideExecutor.newSourceBuilder()
                    .setUncaughtThrowableStrategy(catcher).build()
            )

            builder.setDiskCache(InternalCacheDiskCacheFactory(context, 10L * 1024L * 1024L))

            // DEBUG 画像のディスクキャッシュの消去
            //			new Thread(new Runnable() {
            //				@Override
            //				public void run() {
            //					Glide.get(context).clearDiskCache();
            //				}
            //			}).start();

            //
            //			////////////
            //			// サンプル1：キャッシュサイズを自動で計算する
            //			val calculator = MemorySizeCalculator.Builder(context)
            //				.setMemoryCacheScreens(2f)
            //				.setBitmapPoolScreens(3f)
            //				.build()
            //
            //			builder.setMemoryCache(LruResourceCache(calculator.memoryCacheSize.toLong()))

            //////
            // サンプル2：キャッシュサイズをアプリが決める
            // int memoryCacheSizeBytes = 1024 * 1024 * 20; // 20mb
            // builder.setMemoryCache(new LruResourceCache(memoryCacheSizeBytes));

            //////
            // サンプル3 ： 自前のメモリキャッシュ
            // builder.setMemoryCache(new YourAppMemoryCacheImpl());

            //			builder.setBitmapPool(LruBitmapPool(calculator.bitmapPoolSize.toLong()))

            // ディスクキャッシュを保存する場所を変えたい場合
            // builder.setDiskCache(new ExternalDiskCacheFactory(context));

            // ディスクキャッシュのサイズを変えたい場合
            //			val diskCacheSizeBytes = 1024 * 1024 * 100 // 100 MB
            //			builder.setDiskCache(InternalDiskCacheFactory(context, diskCacheSizeBytes))

            // Although RequestOptions are typically specified per request,
            // you can also apply a default set of RequestOptions that will be applied to every load
            // you start in your application by using an AppGlideModule:
            //			val ro = RequestOptions()
            //				.format(DecodeFormat.PREFER_ARGB_8888)
            //				.disallowHardwareConfig()
            //			builder.setDefaultRequestOptions(ro)
        }

        fun setActivityTheme(
            activity: Activity,
            forceDark: Boolean = false,
        ) {
            prepare(activity.applicationContext, "setActivityTheme")

            var nTheme = PrefI.ipUiTheme.value
            if (forceDark && nTheme == 0) nTheme = 1
            activity.setTheme(
                when (nTheme) {
                    2 -> R.style.AppTheme_Mastodon
                    1 -> R.style.AppTheme_Dark
                    else -> R.style.AppTheme_Light
                }
            )
            activity.setStatusBarColor(forceDark = forceDark)
        }

        internal val CACHE_CONTROL = CacheControl.Builder()
            .maxAge(1, TimeUnit.DAYS) // キャッシュが新鮮であると考えられる時間
            .build()

        suspend fun getHttpCached(url: String): ByteArray? {
            val caller = RuntimeException("caller's stackTrace.")
            val response: Response

            try {
                val request_builder = Request.Builder()
                    .cacheControl(CACHE_CONTROL)
                    .url(url)

                val call = ok_http_client2.newCall(request_builder.build())
                response = call.await()
            } catch (ex: Throwable) {
                log.e(ex, "getHttp network error. $url")
                return null
            }

            if (!response.isSuccessful) {
                log.e(
                    caller,
                    TootApiClient.formatResponse(response, "getHttp response error. $url")
                )
                return null
            }

            return try {
                response.body.bytes()
            } catch (ex: Throwable) {
                log.e(ex, "getHttp content error. $url")
                null
            }
        }

        suspend fun getHttpCachedString(
            url: String,
            accessInfo: SavedAccount? = null,
            misskeyPost: Boolean = false,
            builderBlock: (Request.Builder) -> Unit = {},
        ): String? {
            val response: Response

            try {
                val request_builder = when {
                    misskeyPost && accessInfo?.isMisskey == true ->
                        accessInfo.putMisskeyApiToken().toPostRequestBuilder()
                            .url(url)
                            .cacheControl(CACHE_CONTROL)

                    else ->
                        Request.Builder()
                            .url(url)
                            .cacheControl(CACHE_CONTROL)
                            .also {
                                accessInfo?.bearerAccessToken?.notEmpty()?.let { a ->
                                    it.header("Authorization", "Bearer $a")
                                }
                            }
                }
                builderBlock(request_builder)
                val call = ok_http_client2.newCall(request_builder.build())
                response = call.await()
            } catch (ex: Throwable) {
                log.e(ex, "getHttp network error. $url")
                return null
            }

            if (!response.isSuccessful) {
                log.e(TootApiClient.formatResponse(response, "getHttp response error. $url"))
                return null
            }

            return try {
                response.body.string()
            } catch (ex: Throwable) {
                log.e(ex, "getHttp content error. $url")
                null
            }
        }
    }
}

val kJson = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }
