package com.margelo.nitro.lunardatepicker.ui.fragments import android.annotation.SuppressLint import android.app.Dialog import android.os.Bundle import android.util.Log import android.view.View import android.widget.FrameLayout import android.widget.LinearLayout import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.view.CalendarView import com.kizitonwose.calendar.view.MarginValues import com.kizitonwose.calendar.view.MonthDayBinder import com.kizitonwose.calendar.view.MonthHeaderFooterBinder import com.margelo.nitro.lunardatepicker.LDP_PickerMode import com.margelo.nitro.lunardatepicker.LDP_PriceData import com.margelo.nitro.lunardatepicker.LDP_Range import com.margelo.nitro.lunardatepicker.R import com.margelo.nitro.lunardatepicker.constants.DataConstants import com.margelo.nitro.lunardatepicker.constants.LayoutConstants import com.margelo.nitro.lunardatepicker.models.DateSelection import com.margelo.nitro.lunardatepicker.models.PickerConfig import com.margelo.nitro.lunardatepicker.models.SerializableRange import com.margelo.nitro.lunardatepicker.repositories.PriceRepository import com.margelo.nitro.lunardatepicker.ui.builders.UIBuilder import com.margelo.nitro.lunardatepicker.ui.viewholders.DayViewHolder import com.margelo.nitro.lunardatepicker.ui.viewholders.MonthViewHolder import com.margelo.nitro.lunardatepicker.utils.DateConverter import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth import java.time.ZoneId /** * Refactored BottomSheetDialogFragment for lunar date picker * Follows clean architecture principles with separation of concerns */ class LunarDatePickerFragment : BottomSheetDialogFragment() { companion object { private const val TAG = DataConstants.LogTags.FRAGMENT private const val ARG_CONFIG = DataConstants.BundleKeys.ARG_CONFIG private const val ARG_MODE = DataConstants.BundleKeys.ARG_MODE private const val ARG_MIN_DATE = DataConstants.BundleKeys.ARG_MIN_DATE private const val ARG_MAX_DATE = DataConstants.BundleKeys.ARG_MAX_DATE private const val ARG_INITIAL_VALUE = DataConstants.BundleKeys.ARG_INITIAL_VALUE fun newInstance( config: PickerConfig, mode: LDP_PickerMode, minimumDate: LocalDate? = null, maximumDate: LocalDate? = null, initialValue: LDP_Range? = null, onResult: (LDP_Range) -> Unit, onMonthVisible: ((String) -> Unit)? = null, onSelectFromDate: ((String, Array) -> Unit)? = null ): LunarDatePickerFragment { val fragment = LunarDatePickerFragment() fragment.setupCallbacks(onResult, onMonthVisible, onSelectFromDate) fragment.arguments = createArguments(config, mode, minimumDate, maximumDate, initialValue) return fragment } private fun createArguments( config: PickerConfig, mode: LDP_PickerMode, minimumDate: LocalDate?, maximumDate: LocalDate?, initialValue: LDP_Range? ): Bundle { return Bundle().apply { putSerializable(ARG_CONFIG, config) putSerializable(ARG_MODE, mode) minimumDate?.let { putString(ARG_MIN_DATE, it.toString()) } maximumDate?.let { putString(ARG_MAX_DATE, it.toString()) } initialValue?.let { putSerializable(ARG_INITIAL_VALUE, SerializableRange.fromRange(it)) } } } } // Dependencies private lateinit var config: PickerConfig private lateinit var mode: LDP_PickerMode private lateinit var dateConverter: DateConverter private lateinit var priceRepository: PriceRepository private lateinit var uiBuilder: UIBuilder private lateinit var calendarView: CalendarView // State private var minimumDate: LocalDate? = null private var maximumDate: LocalDate? = null private var initialValue: LDP_Range? = null private var selection = DateSelection() private val timeZone: ZoneId by lazy { config.calendar.timeZone.toZoneId() } // Callbacks private var onResultCallback: ((LDP_Range) -> Unit)? = null private var onMonthVisibleCallback: ((String) -> Unit)? = null private var onSelectFromDateCallback: ((String, Array) -> Unit)? = null // Performance optimization private var isScrolling = false private val pendingPriceUpdates: MutableList = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initializeFromArguments() initializeDependencies() setupInitialSelection() } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val bottomSheetDialog = BottomSheetDialog(requireContext()) val view = createBottomSheetView() bottomSheetDialog.setContentView(view) setupBottomSheetBehavior(bottomSheetDialog) return bottomSheetDialog } override fun onDestroy() { super.onDestroy() cleanup() } /** * Updates prices for a specific month */ fun updatePrices(month: String, prices: List) { val update = PendingPriceUpdate.UpdatePricesPartial(month, prices) if (isScrolling) { handleScrollingPriceUpdate(update) } else { processPriceUpdate(update) } } // Private Methods private fun initializeFromArguments() { arguments?.let { args -> config = args.getSerializable(ARG_CONFIG) as PickerConfig mode = args.getSerializable(ARG_MODE) as LDP_PickerMode minimumDate = args.getString(ARG_MIN_DATE)?.let { LocalDate.parse(it) } maximumDate = args.getString(ARG_MAX_DATE)?.let { LocalDate.parse(it) } val serializableRange = args.getSerializable(ARG_INITIAL_VALUE) as? SerializableRange initialValue = serializableRange?.toRange() } } private fun initializeDependencies() { dateConverter = DateConverter() priceRepository = PriceRepository() uiBuilder = UIBuilder(requireContext(), config) } private fun setupCallbacks( onResult: (LDP_Range) -> Unit, onMonthVisible: ((String) -> Unit)?, onSelectFromDate: ((String, Array) -> Unit)? ) { onResultCallback = onResult onMonthVisibleCallback = onMonthVisible onSelectFromDateCallback = onSelectFromDate } private fun createBottomSheetView(): View { val rootView = LinearLayout(requireContext()).apply { orientation = LinearLayout.VERTICAL setBackgroundColor(config.controller.backgroundColor) } // Add UI components rootView.addView(uiBuilder.createHandleView()) rootView.addView(uiBuilder.createTitleView()) rootView.addView(uiBuilder.createWeekView()) rootView.addView(createCalendarContainer()) return rootView } private fun createCalendarContainer(): FrameLayout { val calendarContainer = FrameLayout(requireContext()).apply { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f ) } calendarView = CalendarView(requireContext()).apply { dayViewResource = R.layout.kiz_day_cell monthMargins = MarginValues(LayoutConstants.Padding.MONTH_VIEW_HORIZONTAL, 0) monthHeaderResource = R.layout.kiz_month_header orientation = RecyclerView.VERTICAL clipToPadding = false } setupCalendarView() calendarContainer.addView( calendarView, FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT ) ) return calendarContainer } private fun setupCalendarView() { setupCalendarBinders() setupCalendarRange() setupCalendarListeners() } private fun setupCalendarBinders() { calendarView.dayBinder = object : MonthDayBinder { override fun create(view: View): DayViewHolder { return DayViewHolder( view = view, config = config, dateConverter = dateConverter, timeZone = timeZone, isDateEnabled = ::isDateEnabled, formatPrice = ::formatPrice, onDateClick = ::handleDateSelection ).apply { pricesByDate = priceRepository.getAllPrices() shouldShowPrices = priceRepository.shouldShowPrices() } } override fun bind(container: DayViewHolder, data: CalendarDay) { container.apply { selectedFromDate = selection.startDate selectedToDate = selection.endDate pricesByDate = priceRepository.getAllPrices() shouldShowPrices = priceRepository.shouldShowPrices() bind(data) } } } calendarView.monthHeaderBinder = object : MonthHeaderFooterBinder { override fun create(view: View) = MonthViewHolder(view, config) override fun bind(container: MonthViewHolder, data: CalendarMonth) = container.bind(data) } } private fun setupCalendarRange() { val currentDate = LocalDate.now() val startMonth = minimumDate?.let { YearMonth.from(it) } ?: YearMonth.from(currentDate.minusYears(config.yearRangeOffset.toLong())) val endMonth = maximumDate?.let { YearMonth.from(it) } ?: YearMonth.from(currentDate.plusYears(config.yearRangeOffset.toLong())) // Use Monday as the first day of the week (instead of Sunday) calendarView.setup(startMonth, endMonth, DayOfWeek.MONDAY) calendarView.scrollToMonth(YearMonth.from(currentDate)) } private fun setupCalendarListeners() { calendarView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) isScrolling = newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING } }) calendarView.monthScrollListener = { calendarMonth -> isScrolling = false processPendingPriceUpdates() notifyMonthVisible(calendarMonth) } } private fun setupBottomSheetBehavior(bottomSheetDialog: BottomSheetDialog) { bottomSheetDialog.setOnShowListener { dialog -> val d = dialog as BottomSheetDialog val bottomSheet = d.findViewById(com.google.android.material.R.id.design_bottom_sheet) bottomSheet?.let { val behavior = BottomSheetBehavior.from(it) behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.skipCollapsed = true behavior.isDraggable = true } scrollToCurrentDateIfNeeded() } } @SuppressLint("DefaultLocale") private fun notifyMonthVisible(calendarMonth: CalendarMonth) { onMonthVisibleCallback?.let { callback -> val currentYearMonth = calendarMonth.yearMonth val maxYearMonth = maximumDate?.let { YearMonth.from(it) } for (i in 0 until DataConstants.Calendar.MONTHS_TO_PRELOAD) { val targetYearMonth = currentYearMonth.plusMonths(i.toLong()) if (maxYearMonth != null && targetYearMonth.isAfter(maxYearMonth)) break val monthKey = String.format( DataConstants.Format.MONTH_KEY, targetYearMonth.year, targetYearMonth.monthValue ) callback.invoke(monthKey) } } } private fun handleDateSelection(date: LocalDate) { when (mode) { LDP_PickerMode.SINGLE -> handleSingleDateSelection(date) LDP_PickerMode.RANGE -> handleRangeDateSelection(date) } } private fun handleSingleDateSelection(date: LocalDate) { selection = DateSelection(startDate = date, endDate = date) calendarView.notifyCalendarChanged() onResultCallback?.invoke(LDP_Range(from = dateConverter.stringFromDate(date), to = null)) dismiss() } private fun handleRangeDateSelection(date: LocalDate) { val shouldComplete = processRangeSelection(date) if (shouldComplete) { onResultCallback?.invoke( LDP_Range( from = dateConverter.stringFromDate(selection.startDate!!), to = selection.endDate?.let { dateConverter.stringFromDate(it) } ) ) dismiss() } else { calendarView.notifyCalendarChanged() } } private fun processRangeSelection(date: LocalDate): Boolean { val currentFromDate = selection.startDate var shouldComplete = false when { selection.startDate == null -> { selection = DateSelection(startDate = date, endDate = null) } selection.startDate!! > date -> { selection = DateSelection(startDate = date, endDate = null) } selection.endDate == null -> { val startDate = selection.startDate!! if (date.isBefore(startDate)) { selection = DateSelection(startDate = date, endDate = startDate) } else { selection = DateSelection(startDate = startDate, endDate = date) } shouldComplete = true } else -> { selection = DateSelection(startDate = date, endDate = null) } } // Notify from date selection if needed if (!shouldComplete && (currentFromDate == null || selection.startDate != currentFromDate)) { notifyFromDateSelection() } return shouldComplete } private fun notifyFromDateSelection() { onSelectFromDateCallback?.let { callback -> val fromDateString = dateConverter.stringFromDate(selection.startDate!!) val currentlyVisibleMonths = getCurrentlyVisibleMonths() callback(fromDateString, currentlyVisibleMonths) } } @SuppressLint("DefaultLocale") private fun getCurrentlyVisibleMonths(): Array { val currentMonth = YearMonth.now() val monthKey = String.format( DataConstants.Format.MONTH_KEY, currentMonth.year, currentMonth.monthValue ) return arrayOf(monthKey) } private fun handleScrollingPriceUpdate(update: PendingPriceUpdate) { val existingIndex = pendingPriceUpdates.indexOfFirst { it is PendingPriceUpdate.UpdatePricesPartial && it.month == (update as PendingPriceUpdate.UpdatePricesPartial).month } if (existingIndex != -1) { pendingPriceUpdates[existingIndex] = update } else { pendingPriceUpdates.add(update) } } private fun processPendingPriceUpdates() { if (pendingPriceUpdates.isNotEmpty()) { pendingPriceUpdates.forEach { processPriceUpdate(it) } pendingPriceUpdates.clear() } } private fun processPriceUpdate(update: PendingPriceUpdate) { when (update) { is PendingPriceUpdate.UpdatePricesPartial -> { priceRepository.updatePricesForMonth(update.month, update.prices) if (::calendarView.isInitialized) { try { val yearMonth = YearMonth.parse(update.month) calendarView.post { calendarView.notifyMonthChanged(yearMonth) } } catch (e: Exception) { Log.e(TAG, "Failed to parse month ${update.month}", e) calendarView.notifyCalendarChanged() } } } } } private fun setupInitialSelection() { initialValue?.let { value -> val fromDate = dateConverter.dateFromString(value.from) fromDate?.let { selection = selection.copy(startDate = it) } value.to?.let { toValue -> val toDate = dateConverter.dateFromString(toValue) toDate?.let { selection = selection.copy(endDate = it) } } } } private fun scrollToCurrentDateIfNeeded() { val targetDate = initialValue?.let { dateConverter.dateFromString(it.from) } ?: LocalDate.now().let { current -> maximumDate?.let { max -> if (max.isBefore(current)) max else current } ?: current } calendarView.post { calendarView.scrollToMonth(YearMonth.from(targetDate)) } } private fun isDateEnabled(date: LocalDate): Boolean { minimumDate?.let { if (date.isBefore(it)) return false } maximumDate?.let { if (date.isAfter(it)) return false } return true } @SuppressLint("DefaultLocale") private fun formatPrice(price: Double): String { val millions = price / DataConstants.Numeric.MILLION_DIVIDER if (millions >= 1) { return String.format(DataConstants.Format.PRICE_MILLION, millions) } val thousands = price / DataConstants.Numeric.THOUSAND_DIVIDER return String.format(DataConstants.Format.PRICE_THOUSAND, thousands) } private fun cleanup() { onResultCallback = null onMonthVisibleCallback = null onSelectFromDateCallback = null pendingPriceUpdates.clear() priceRepository.clearPrices() } /** * Sealed class for pending price updates */ sealed class PendingPriceUpdate { data class UpdatePricesPartial(val month: String, val prices: List) : PendingPriceUpdate() } }