// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.dashboard.ui.map

import android.annotation.SuppressLint
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_UNDEFINED
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.edit
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.snackbar.Snackbar
import org.osmdroid.config.Configuration
import org.osmdroid.events.MapListener
import org.osmdroid.events.ScrollEvent
import org.osmdroid.events.ZoomEvent
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.TilesOverlay
import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.dashboard.MainActivity
import xyz.apiote.bimba.czwek.databinding.FragmentMapBinding
import xyz.apiote.bimba.czwek.dpToPixelI
import xyz.apiote.bimba.czwek.repo.ErrorLocatable
import xyz.apiote.bimba.czwek.repo.Position
import xyz.apiote.bimba.czwek.repo.Stop
import xyz.apiote.bimba.czwek.repo.Vehicle


class MapFragment : Fragment() {

	companion object {
		const val PREFERENCES_NAME = "shp"
		const val ZOOM_KEY = "mapZoom"
		const val CENTRE_LATITUDE_KEY = "mapCentreLat"
		const val CENTRE_LONGITUDE_KEY = "mapCentreLon"
	}

	private var maybeBinding: FragmentMapBinding? = null
	private val binding get() = maybeBinding!!

	private lateinit var locationOverlay: MyLocationNewOverlay
	private lateinit var mapViewModel: MapViewModel

	private val handler = Handler(Looper.getMainLooper())
	private var workRunnable = Runnable {}

	private var snack: Snackbar? = null

	@SuppressLint("ClickableViewAccessibility")
	override fun onCreateView(
		inflater: LayoutInflater,
		container: ViewGroup?,
		savedInstanceState: Bundle?
	): View {
		mapViewModel =
			ViewModelProvider(this)[MapViewModel::class.java]

		observeLocatables()

		maybeBinding = FragmentMapBinding.inflate(inflater, container, false)
		val root: View = binding.root

		binding.map.setTileSource(TileSourceFactory.MAPNIK)
		if (((context?.resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED)
				and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
		) {
			binding.map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
		}
		binding.map.zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
		binding.map.maxZoomLevel = 21.5
		binding.map.minZoomLevel = 5.5
		binding.map.setMultiTouchControls(true)
		binding.map.overlays.add(RotationGestureOverlay(binding.map).apply { isEnabled = true })

		locationOverlay = MyLocationNewOverlay(GpsMyLocationProvider(context), binding.map)
		context?.let {
			centreMap(it.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE))

			locationOverlay.setDirectionIcon(
				AppCompatResources.getDrawable(it, R.drawable.navigation_arrow)?.mutate()
					?.toBitmap(dpToPixelI(36f), dpToPixelI(36f), Bitmap.Config.ARGB_8888)
			)
			locationOverlay.setDirectionAnchor(.5f, .5f)
			locationOverlay.setPersonIcon(
				AppCompatResources.getDrawable(it, R.drawable.navigation_circle)?.mutate()
					?.toBitmap(dpToPixelI(24f), dpToPixelI(24f), Bitmap.Config.ARGB_8888)
			)
			locationOverlay.setPersonAnchor(.5f, .5f)
		}

		binding.floatingActionButton.setOnClickListener {
			(context as MainActivity).onGpsClicked(this)
		}

		binding.map.addMapListener(object : MapListener {
			override fun onScroll(event: ScrollEvent?): Boolean {
				onMapMove()
				return true
			}

			override fun onZoom(event: ZoomEvent?): Boolean {
				onMapMove()
				return true
			}
		})

		binding.map.setOnTouchListener { _, _ ->
			binding.floatingActionButton.show()
			false
		}

		val attributionOverlay = TextView(context).apply {
			text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
				Html.fromHtml(getString(R.string.map_attribution), 0)
			} else {
				@Suppress("DEPRECATION")
				Html.fromHtml(getString(R.string.map_attribution))
			}
			linksClickable = true
			movementMethod = LinkMovementMethod.getInstance()
			setPadding(10, 10, 10, 10)
		}
		val layoutParams = CoordinatorLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
			gravity = Gravity.END or Gravity.BOTTOM
		}
		binding.mapFragmentRoot.addView(
			attributionOverlay,
			layoutParams
		)
		attributionOverlay.bringToFront()

		ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
			binding.floatingActionButton.updateLayoutParams<ViewGroup.MarginLayoutParams> {
				rightMargin += insets.right
			}
			attributionOverlay.updateLayoutParams<ViewGroup.MarginLayoutParams> {
				rightMargin += insets.right
			}
			windowInsets
		}

		return root
	}

	private fun onMapMove() {
		snack?.dismiss()
		delayGetLocatables()
	}

	private fun delayGetLocatables(delay: Long = 1000) {
		handler.removeCallbacks(workRunnable)
		workRunnable = Runnable {
			getLocatables()
		}
		handler.postDelayed(workRunnable, delay)
	}

	private fun observeLocatables() {
		mapViewModel.locatables.observe(viewLifecycleOwner) {
			binding.map.overlays.removeAll { marker ->
				marker is Marker
			}

			if (it.size == 1 && it[0] is ErrorLocatable) {
				Snackbar.make(binding.root, (it[0] as ErrorLocatable).stringResource, Snackbar.LENGTH_LONG)
					.show()
				return@observe
			}

			it.forEach { locatable ->
				val marker = Marker(binding.map)
				marker.position = GeoPoint(locatable.location().latitude, locatable.location().longitude)
				when (locatable) {
					is Stop -> marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
					is Vehicle -> marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
				}

				val scale = binding.map.zoomLevelDouble / -4 + 5.5
				marker.icon = context?.let { ctx -> locatable.icon(ctx, scale.toFloat()) }

				context?.let { ctx ->
					marker.setOnMarkerClickListener { _, _ ->
						MapBottomSheet(locatable).apply {
							(ctx as MainActivity?)?.supportFragmentManager?.let { fm ->
								show(fm, MapBottomSheet.TAG)
							}
						}
						true
					}
				}
				binding.map.overlays.add(marker)
			}

			binding.map.invalidate()
		}
	}

	fun showLocation() {
		snack =
			Snackbar.make(binding.root, getString(R.string.waiting_position), Snackbar.LENGTH_INDEFINITE)
		snack!!.show()
		binding.floatingActionButton.hide()
		binding.map.overlays.removeAll {
			it is MyLocationNewOverlay
		}
		locationOverlay.enableFollowLocation()
		binding.map.overlays.add(locationOverlay)
		locationOverlay.runOnFirstFix {
			snack?.dismiss()
		}
	}

	private fun getLocatables() {
		maybeBinding?.let { binding ->
			val (bl, tr) = binding.map.boundingBox.let {
				Pair(
					Position(it.latSouth, it.lonWest),
					Position(it.latNorth, it.lonEast)
				)
			}
			context?.let {
				mapViewModel.getLocatablesIn(bl, tr, it)
			}
			delayGetLocatables(30000)
		}
	}

	private fun centreMap(preferences: SharedPreferences) {
		maybeBinding?.map?.controller?.apply {
			setZoom(preferences.getFloat(ZOOM_KEY, 17.0f).toDouble())
			val startPoint = GeoPoint(
				preferences.getFloat(CENTRE_LATITUDE_KEY, 52.39511f).toDouble(),
				preferences.getFloat(CENTRE_LONGITUDE_KEY, 16.89506f).toDouble()
			)
			setCenter(startPoint)
		}
	}

	override fun onResume() {
		super.onResume()
		binding.map.onResume()
		locationOverlay.enableMyLocation()
		context?.let { ctx ->
			ctx.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE).let {
				Configuration.getInstance()
					.load(ctx, it)
				centreMap(it)
			}
		}
	}

	override fun onPause() {
		super.onPause()
		binding.map.onPause()
		locationOverlay.disableMyLocation()
		val centre = binding.map.mapCenter
		context?.let { ctx ->
			ctx.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE).edit(true) {
				putFloat(CENTRE_LATITUDE_KEY, centre.latitude.toFloat())
				putFloat(CENTRE_LONGITUDE_KEY, centre.longitude.toFloat())
				putFloat(ZOOM_KEY, binding.map.zoomLevelDouble.toFloat())
			}
		}
		handler.removeCallbacks(workRunnable)
	}

	override fun onDestroyView() {
		super.onDestroyView()
		maybeBinding = null
	}
}
