package com.laseroptek.raman.ui.screens.main

import android.content.Context
import android.media.AudioManager
import android.media.MediaPlayer
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.laseroptek.raman.BuildConfig
import com.laseroptek.raman.R
import com.laseroptek.raman.const.CMD
import com.laseroptek.raman.const.CMD.CMD_FLAG
import com.laseroptek.raman.const.CMD.WRITE_FLAG
import com.laseroptek.raman.const.DEFAULT_GUIDE_BEAM
import com.laseroptek.raman.const.EnergyTable_10_10
import com.laseroptek.raman.const.EnergyTable_12_12
import com.laseroptek.raman.const.EnergyTable_3_15
import com.laseroptek.raman.const.EnergyTable_5_5
import com.laseroptek.raman.const.EnergyTable_7_7
import com.laseroptek.raman.const.Fluences
import com.laseroptek.raman.const.HzTable_10_10
import com.laseroptek.raman.const.HzTable_12_12
import com.laseroptek.raman.const.HzTable_3_15
import com.laseroptek.raman.const.HzTable_5_5
import com.laseroptek.raman.const.HzTable_7_7
import com.laseroptek.raman.const.KEY_YELLOW
import com.laseroptek.raman.const.LASER_STATUS
import com.laseroptek.raman.const.LaserParameter
import com.laseroptek.raman.const.MAX_CHART_COLUMN_DATA_SIZE
import com.laseroptek.raman.const.MAX_GUIDE_BEAM
import com.laseroptek.raman.const.MAX_LAMP_COUNT
import com.laseroptek.raman.const.MAX_LOG_COUNT
import com.laseroptek.raman.const.MIN_GUIDE_BEAM
import com.laseroptek.raman.const.MinMaxUpDownState
import com.laseroptek.raman.const.PresetList
import com.laseroptek.raman.const.PulseDurations
import com.laseroptek.raman.const.READ_WRITE
import com.laseroptek.raman.const.RX_TIMEOUT_THRESHOLD
import com.laseroptek.raman.const.RepetitionsByColorKey
import com.laseroptek.raman.const.SERIAL_BAUDRATE
import com.laseroptek.raman.const.SERIAL_PORT_NAME
import com.laseroptek.raman.const.SHOW_ENERGY_DETECT
import com.laseroptek.raman.const.SprayDcdList
import com.laseroptek.raman.const.UpDownState
import com.laseroptek.raman.const.VoltageTable
import com.laseroptek.raman.const.txRepeatMillis
import com.laseroptek.raman.data.model.DcdType
import com.laseroptek.raman.data.model.LifeTime
import com.laseroptek.raman.data.model.Preset
import com.laseroptek.raman.data.model.Refer2
import com.laseroptek.raman.data.model.SerialResult
import com.laseroptek.raman.data.model.serial.Alert
import com.laseroptek.raman.data.model.serial.DcdGas
import com.laseroptek.raman.data.model.serial.DcdGasOnOff
import com.laseroptek.raman.data.model.serial.EnergyControl
import com.laseroptek.raman.data.model.serial.EnergyControlStop
import com.laseroptek.raman.data.model.serial.EnergyHandpiece
import com.laseroptek.raman.data.model.serial.EnergyHandpieceRemove
import com.laseroptek.raman.data.model.serial.EnergyMeasured
import com.laseroptek.raman.data.model.serial.EnergyVersion
import com.laseroptek.raman.data.model.serial.GuideBeam
import com.laseroptek.raman.data.model.serial.HandPiece
import com.laseroptek.raman.data.model.serial.LaserStatus
import com.laseroptek.raman.data.model.serial.Oven
import com.laseroptek.raman.data.model.serial.Packet
import com.laseroptek.raman.data.model.serial.PurgeBubble
import com.laseroptek.raman.data.model.serial.QSwitch
import com.laseroptek.raman.data.model.serial.SprayDcd
import com.laseroptek.raman.data.model.serial.Temperature
import com.laseroptek.raman.data.model.serial.TemperatureTimeStamp
import com.laseroptek.raman.data.model.serial.Version
import com.laseroptek.raman.data.source.db.model.SerialLog
import com.laseroptek.raman.repository.DatabaseRepository
import com.laseroptek.raman.repository.PreferenceRepository
import com.laseroptek.raman.repository.SerialPortRepository
import com.laseroptek.raman.utils.DispatcherProvider
import com.laseroptek.raman.utils.FixedSizeLIFOQueue
import com.laseroptek.raman.utils.SystemSound.setCurrentSystemVolume
import com.laseroptek.raman.utils.ext.degreeToStep
import com.laseroptek.raman.utils.ext.getCmdString
import com.laseroptek.raman.utils.ext.getHexString
import com.laseroptek.raman.utils.ext.getKey2ListForKey1
import com.laseroptek.raman.utils.ext.getValue
import com.laseroptek.raman.utils.ext.mask
import com.laseroptek.raman.utils.ext.printHex
import com.laseroptek.raman.utils.ext.setVoltageTableValue
import com.laseroptek.raman.utils.ext.stepToDegree
import com.laseroptek.raman.utils.ext.toByteArray
import com.laseroptek.raman.utils.ext.toCS
import com.laseroptek.raman.utils.ext.toDcdGas
import com.laseroptek.raman.utils.ext.toEnergyControl
import com.laseroptek.raman.utils.ext.toEnergyHandpiece
import com.laseroptek.raman.utils.ext.toEnergyMeasured
import com.laseroptek.raman.utils.ext.toEnergyVersion
import com.laseroptek.raman.utils.ext.toError
import com.laseroptek.raman.utils.ext.toFluenceMutableStateFlowMap
import com.laseroptek.raman.utils.ext.toHandPiece
import com.laseroptek.raman.utils.ext.toHpShotCount
import com.laseroptek.raman.utils.ext.toInt
import com.laseroptek.raman.utils.ext.toLaserStatus
import com.laseroptek.raman.utils.ext.toMutableStateFlow
import com.laseroptek.raman.utils.ext.toOven
import com.laseroptek.raman.utils.ext.toPacketResult
import com.laseroptek.raman.utils.ext.toPurgeBubble
import com.laseroptek.raman.utils.ext.toQSwitch
import com.laseroptek.raman.utils.ext.toSprayDcd
import com.laseroptek.raman.utils.ext.toTemperature
import com.laseroptek.raman.utils.ext.toVersion
import com.laseroptek.raman.utils.ext.toVoltageMutableStateFlowMap
import com.laseroptek.raman.utils.ext.toWarning
import com.laseroptek.raman.utils.ext.toYYYYMMDDHHMMSSString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import kotlin.experimental.or
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit

@HiltViewModel
class MainViewModel @Inject constructor(
    private val preferenceRepository: PreferenceRepository,
    private val serialPortRepository: SerialPortRepository,
    private val databaseRepository: DatabaseRepository,
    private val dispatcherProvider: DispatcherProvider,
    private val applicationContext: Context,
) : ViewModel() {

    /////////////////////////////////////////////////////////////////////////////////f///////////////
    // MainActivity -

    // apkUpdateEvent
    private val _apkUpdateEvent = MutableSharedFlow<Unit>()
    val apkUpdateEvent: SharedFlow<Unit> = _apkUpdateEvent.asSharedFlow()
    suspend fun emitUpdateApkEvent() {  // EngineerMode - onUpdateClick
        Timber.d("apkUpdateEvent emit")
        _apkUpdateEvent.emit(Unit) // Emit an event
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // show Popups
    var showEnergyDetectPopup by mutableStateOf(SHOW_ENERGY_DETECT)

    var showWarningPopup by mutableStateOf(false)
    var showResetLaserCountPopup by mutableStateOf(false)
    var showPresetLoadPopup by mutableStateOf(false)
    var showPresetSavePopup by mutableStateOf(false)
    var showDcdSettingPopup by mutableStateOf(false)
    var showStandByPopup by mutableStateOf(false)
    var showEngineerPasswordPopup by mutableStateOf(false)
    var showLanguageScreen by mutableStateOf(false)
    var showTimeSettingPopup by mutableStateOf(false)

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // MainScreen -

    // initialization flag (to prevent too much work on its main thread on initialize)
    private val _isInitialized = MutableStateFlow(false)
    val isInitialized = _isInitialized.asStateFlow()
    fun setInitialized(value: Boolean) { _isInitialized.value = value }

    // voltageTable - (PulseWidth(ms), Energy(J)) -> Voltage(V)
    // - Engineer Mode에서 Energy 값 변경 시 - voltageTable 변경 및 Preference 저장
    // - Main 화면 StandBy 선택시, laser Status 값에 전달할 Voltage 계산시 사용 됨.
    // - 앱 실행시 저장된 테이블을  preference 에서 값 해당 값 로딩 (없으면 기본값 = 상수값 = 공장초기화 값)
    private val _voltageTable: MutableStateFlow<MutableMap<Pair<Float, Float>, MutableStateFlow<Int>>> =
        VoltageTable.toVoltageMutableStateFlowMap()
    val voltageTable = _voltageTable.asStateFlow()
    fun setVoltageTable(v: MutableMap<Pair<Float, Float>, MutableStateFlow<Int>>) {
        _voltageTable.value = v
        v.forEach {
            _voltageTable.value.setVoltageTableValue(it.key, it.value.value)
        }
    }
    fun getVoltage(pulseWidth: Float, energy: Float): Int {
        val t = _voltageTable.value
        val v = t.getValue(pulseWidth, energy)?.value ?: 0
        return v
    }


    // energyTable - (PulseWidth(ms), Fluence(J/cm²)) -> Energy(J)
    // - Handpiece 타입에 따라 각각의 Energy Table로 구분
    // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 energyTable 업데이트
    // - 앱 실행시 저장된 테이블에서 값 해당 값 로딩 (없으면 기본값 = 상수값 = 공장초기화 값)
    private val _energyTable: MutableStateFlow<MutableMap<Pair<Float, Float>, Float>> =
        EnergyTable_5_5.toFluenceMutableStateFlowMap()
    val energyTable = _energyTable.asStateFlow()
    fun setEnergyTable(e: MutableMap<Pair<Float, Float>, Float>) {
        _energyTable.value = e
    }

    // fluenceList - handpiece type에 따른 Fluence 단계 값 (Guage 단계값 테이블)
    // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 energyTable 업데이트
    // - Fluences는 5_5 일때의 목록 값 (초기값: handPeiece none 상태)
    // - ref. Repetitions, PulseDurations는 fixed Table 임
    private val _fluenceList: MutableStateFlow<List<Float>> =
        Fluences.toMutableStateFlow()
    val fluenceList = _fluenceList.asStateFlow()
    fun setFluenceList(f: List<Float>) {
        _fluenceList.value = f
    }

    // hzTable - (PulseWidth(ms), Fluence(J/cm²)) -> hzType(int)
    // - Handpiece 타입에 따라 각각의 Energy Table로 구분
    // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 hzTable 업데이트
    // - 앱 실행시 저장된 테이블에서 값 해당 값 로딩 (없으면 기본값 = 상수값 = 공장초기화 값)
    private val _hzTable: MutableStateFlow<MutableMap<Pair<Float, Float>, Int>> =
        HzTable_5_5.toFluenceMutableStateFlowMap()
    val hzTable = _hzTable.asStateFlow()
    fun setHzTable(h: MutableMap<Pair<Float, Float>, Int>) {
        _hzTable.value = h
    }

    // repetitionList - handpiece type에 따른 Repetition 단계 값 (Guage 단계값 테이블)
    // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 hzTable 업데이트
    // - Repetition은 5_5 일때의 목록 값 (초기값: handPeiece none 상태)
    // - ref. PulseDurations는 fixed Table 임
    private val _repetitionList: MutableStateFlow<List<Float>> =
        RepetitionsByColorKey[KEY_YELLOW]!!.toMutableStateFlow()
    val repetitionList = _repetitionList.asStateFlow()
    fun setRepetitionList(r: List<Float>) {
        _repetitionList.value = r
    }

    // pulseAngle, fluenceAngle, repetitionAngle - Laser Slider (Guage)
    // - 메인 화면의 Guage 표시.
    // - 현재 설정된 각각의 값들의 단계별 각도 값을 유지 한다.

    private val _pulseAngle: MutableStateFlow<Float> = MutableStateFlow(0f)
    val pulseAngle = _pulseAngle.asStateFlow()
    fun setPulseAngle(a: Float) { _pulseAngle.value = a }

    private val _fluenceAngle: MutableStateFlow<Float> = MutableStateFlow(0f)
    val fluenceAngle = _fluenceAngle.asStateFlow()
    fun setFluenceAngle(a: Float) { _fluenceAngle.value = a }

    private val _repetitionAngle: MutableStateFlow<Float> = MutableStateFlow(0f)
    val repetitionAngle = _repetitionAngle.asStateFlow()
    fun setRepetitionAngle(a: Float) { _repetitionAngle.value = a }

    // Define the custom comparator
    private val presetComparator = Comparator<Preset> { a, b ->
        val aIsZero = a.priority == 0
        val bIsZero = b.priority == 0

        when {        // If a's priority is 0 and b's is not, a comes after b.
            aIsZero && !bIsZero -> 1
            // If b's priority is 0 and a's is not, a comes before b.
            !aIsZero && bIsZero -> -1
            // Otherwise (both are 0 or both are non-zero), sort by priority,
            // then by handPieceType as a secondary sort.
            else -> {
                val priorityCompare = a.priority.compareTo(b.priority)
                if (priorityCompare != 0) {
                    priorityCompare
                } else {
                    a.handPieceType.compareTo(b.handPieceType)
                }
            }
        }
    }

    private val _presetList = MutableStateFlow<List<Preset>>(
        // Initialize with a deep copy of PresetList, sorted as per your original logic
        PresetList.map { it.copy() }
            .sortedWith(presetComparator) // Primary sort by priority, secondary by handPieceType
    )
    val presetList = _presetList
    fun setPresetList(list: List<Preset> = PresetList) {
        // deep copy
        _presetList.value = list
            .map{ it.copy() }
    }
    fun setPresetListSorted(list: List<Preset> = PresetList) {
        // deep copy
        _presetList.value = list
            .map{ it.copy() }
            .sortedWith(presetComparator)
    }
    fun deletePreset(index: Int) {
        val currentList = _presetList.value.toMutableList()
        if (index >= 0 && index < currentList.size) {
            currentList.removeAt(index)
            //_presetList.value = currentList // Assign new list to StateFlow
            setPresetList(currentList)
        } else {
            Timber.e("deletePreset: Invalid index $index for list size ${currentList.size}")
        }
    }
    fun addPreset(preset: Preset) {
        val currentList = _presetList.value.toMutableList()
        currentList.add(preset.copy()) // Add a copy to ensure immutability if preset is reused
        // Re-sort if adding can change order, or decide on insertion logic
        _presetList.value = currentList.sortedWith(presetComparator)
    }
    fun updatePresetName(index: Int, newName: String) {
        val currentList = _presetList.value.toMutableList()
        if (index >= 0 && index < currentList.size) {
            val updatedPreset = currentList[index].copy(name = newName)
            currentList[index] = updatedPreset
            //_presetList.value = currentList // Assign new list to StateFlow
            setPresetList(currentList)
            Timber.d("Updated preset at index $index with name: $newName. List: ${_presetList.value}")
        } else {
            Timber.e("updatePresetName: Invalid index $index for list size ${currentList.size}")
        }
    }

    // selectedPresetIndex - 선택된 preset button Index
    // - 초기값 = 0 (un selected)
    private val _selectedPresetIndex: MutableStateFlow<Int> = MutableStateFlow(0)
    val selectedPresetIndex = _selectedPresetIndex.asStateFlow()
    fun setSelectedPresetIndex(i: Int) { _selectedPresetIndex.value = i }

    // sprayDcdList - Spreay DCD Option List (6 Fixed Size List)
    var sprayDcdList = SprayDcdList.toMutableStateList()
    fun setSprayDcdList(list: List<SprayDcd>) {
        sprayDcdList = list.map{ it.copy() }.toMutableStateList()
    }
    fun setSprayDcd(idx: Int, item: SprayDcd) {
        sprayDcdList[idx] = item.copy()
    }

    // selectedSprayDcdIndex - 메인 화면 DCD { Spray | Delay }
    // - DcdSettingPopup에서 선택한 sprayDcdList index
    private val _selectedSprayDcdIndex: MutableStateFlow<Int> = MutableStateFlow(1)
    val selectedSprayDcdIndex = _selectedSprayDcdIndex.asStateFlow()
    fun setSelectedSprayDcdIndex(i: Int) { _selectedSprayDcdIndex.value = i }

    // laserCount - # of laser count
    private val _laserCount = MutableStateFlow(0)
    val laserCount = _laserCount.asStateFlow()
    fun setLaserCount(c: Int) { _laserCount.value = c }
    fun increaseLaserCount() { _laserCount.value++ }

    // lampCount - tot # of laser count (reset not allowed)
    private val _lampCount = MutableStateFlow(0)
    val lampCount = _lampCount.asStateFlow()
    fun setLampCount(c: Int) { _lampCount.value = c }
    fun increaseLampCount() {
        if (_lampCount.value < MAX_LAMP_COUNT) {
            _lampCount.value++
        } else {
            Timber.w("Lamp count has reached its maximum limit of $MAX_LAMP_COUNT.")
            txPacket(READ_WRITE.WRITE, CMD.LASER_STATUS, byteArrayOf(LASER_STATUS.STAND_BY.toByte()))

            // laser 상태 초기화 (stand by: 0x53)
            setLaserStatus(laserStatus.value.copy (
                laserStatus = 0x53
            ))
        }
    }

    // dcdCount - # of dcd cooler shot
    private val _dcdCount = MutableStateFlow(0)
    val dcdCount = _dcdCount.asStateFlow()
    fun setDcdCount(c: Int) { _dcdCount.value = c }
    fun increaseDcdCount() { _dcdCount.value++ }

    // hpCount - # of hp count
    private val _hpCount = MutableStateFlow(999000)
    val hpCount = _hpCount.asStateFlow()
    fun setHpCount(c: Int) { _hpCount.value = c }
    fun increaseHpCount() { _hpCount.value++ }

    fun getHPCount(): Int {
        val remainingCount = when (handPiece.value.type) {
            1 -> lifeTime.value.hp5x5 - hpCount.value
            2 -> lifeTime.value.hp7x7 - hpCount.value
            3 -> lifeTime.value.hp10x10 - hpCount.value
            4 -> lifeTime.value.hp12x12 - hpCount.value
            5 -> lifeTime.value.hp3x15 - hpCount.value
            else -> return 0 // Return 6 spaces if the type is unknown
        }

        return remainingCount.coerceAtLeast(0)
    }

    fun getHPCountForMainScreen(): String {
        val remainingCount = when (handPiece.value.type) {
            2 -> lifeTime.value.hp7x7 - hpCount.value
            3 -> lifeTime.value.hp10x10 - hpCount.value
            4 -> lifeTime.value.hp12x12 - hpCount.value
            5 -> lifeTime.value.hp3x15 - hpCount.value
            else -> lifeTime.value.hp5x5 - hpCount.value
        }

        // Ensure the result is not negative for display
        val displayCount = maxOf(0, remainingCount)

        if (displayCount > 1000) {
            return "%4d K".format(displayCount/1000)    // 1000
        } else {
            return "%4d".format(displayCount)
        }
    }

    // lifeTime - Reserved for future use (Int[8])
    private val _lifeTime: MutableStateFlow<LifeTime> = MutableStateFlow(LifeTime())
    val lifeTime = _lifeTime.asStateFlow()
    fun setLifeTime(t: LifeTime) {
        _lifeTime.value = t
    }

    // dcdType - DCD Can Type & LifeSpan
    // - Engineer Mode에서 설정하고, 해당 값으로 DCD Settings에서 Expected Value 계산함.
    private val _dcdType: MutableStateFlow<DcdType> = MutableStateFlow(DcdType())
    val dcdType = _dcdType.asStateFlow()
    fun setDcdType(d: DcdType) {
        _dcdType.value = d
    }
    val gasChargeRate: StateFlow<Float> = combine(_dcdCount, _dcdType) { count, type ->
        val canFactor = if (type.canType == 700) 1 else 2
        val totalCapacity = canFactor * (6000 * type.lifeSpan)

        val gasUsageRate = if (totalCapacity > 0) {
            count.toFloat() / totalCapacity.toFloat()
        } else {
            0f
        }

        val chargeRate = 100f - (gasUsageRate * 100f) // Ensure it's a percentage

        Timber.d("Recalculated gasChargeRate: $chargeRate (Count: $count, Type: ${type.canType})")
        chargeRate.coerceIn(0f, 100f) // Keep between 0 and 100
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = 100f
    )

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // Info Screen (Shared)

    private val _chartDataQueue =
        MutableStateFlow(FixedSizeLIFOQueue<TemperatureTimeStamp>(MAX_CHART_COLUMN_DATA_SIZE))
    val chartDataQueue = _chartDataQueue.asStateFlow()
    fun setChartDataQueue(data: FixedSizeLIFOQueue<TemperatureTimeStamp>) {
        _chartDataQueue.value = data
    }
    fun addChartDataListQueue(dataList: List<TemperatureTimeStamp>) {
        _chartDataQueue.value.addAll(dataList)
    }


    ////////////////////////////////////////////////////////////////////////////////////////////////
    // Engineer Screen (Shared)

    // serialLogs
    private val _serialLogList: MutableStateFlow<List<SerialLog>> =
        MutableStateFlow<List<SerialLog>>(emptyList())
    val serialLogList = _serialLogList.asStateFlow()
    fun setSerialLogList(list: List<SerialLog>) {
        _serialLogList.value = list.toList() // Creates a new list instance
    }

    // apkFile
    private val _apkFile: MutableState<String> = mutableStateOf("")
    val apkFile: MutableState<String> = mutableStateOf("")
    fun setApkFile(file: String) {
        _apkFile.value = file
    }

    // productSerialList
    private val _productSerialList: MutableStateFlow<List<String>> = MutableStateFlow(listOf("B","U","O","C","L","D"))
    val productSerialList = _productSerialList
    fun setProductSerialList(list: List<String>) {
        _productSerialList.value = list.toList() // Creates a new list instance
    }

    // laserHandSerialList
    val _laserHandSerialList: MutableStateFlow<List<String>> = MutableStateFlow(listOf("B","U","O","C","L","D"))
    val laserHandSerialList = _laserHandSerialList
    fun setLaserHandSerialList(list: List<String>) {
        _laserHandSerialList.value = list.toList() // Creates a new list instance
    }

    // powerSupplySerialList
    val _powerSupplySerialList: MutableStateFlow<List<String>> = MutableStateFlow(listOf("B","U","O","C","L","D"))
    val powerSupplySerialList = _powerSupplySerialList
    fun setPowerSupplySerialList(list: List<String>) {
        _powerSupplySerialList.value = list.toList() // Creates a new list instance
    }

    val lastCheckRealTimeMillis: MutableState<Long> = mutableStateOf(System.currentTimeMillis())

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // CONFIG -

    private val _sliderVolume = MutableStateFlow(0f) // Keep the MutableStateFlow private
    val sliderVolume = _sliderVolume.asStateFlow()         // Expose an immutable StateFlow
    fun setSliderVolume(volume: Float) {
        viewModelScope.launch {
            _sliderVolume.value = volume
        }
    }

    // guideBeam (Slider)
    private val _guideBeam = MutableStateFlow(DEFAULT_GUIDE_BEAM.toFloat())
    val guideBeam = _guideBeam.asStateFlow()
    fun setGuideBeam(value: Float) {
        _guideBeam.value = value
    }

    // guideBeamMin
    private val _guideBeamMin = MutableStateFlow(MIN_GUIDE_BEAM)
    val guideBeamMin = _guideBeamMin.asStateFlow()
    fun setGuideBeamMin(value: Int) {
        _guideBeamMin.value = value
    }

    // guideBeamMax
    private val _guideBeamMax = MutableStateFlow(MAX_GUIDE_BEAM)
    val guideBeamMax = _guideBeamMax.asStateFlow()
    fun setGuideBeamMax(value: Int) {
        _guideBeamMax.value = value
    }
    suspend fun onGuideBeamChanged(state: MinMaxUpDownState) {
        Timber.d("onGuideBeamChanged: $state")

        val currentMin = guideBeamMin.value
        val currentMax = guideBeamMax.value

        when (state) {
            MinMaxUpDownState.MinDown -> if (currentMin >= 10) {
                setGuideBeamMin(currentMin - 10)
            }
            MinMaxUpDownState.MinUp -> if (currentMin < MAX_GUIDE_BEAM && (currentMin + 10) < currentMax) {
                setGuideBeamMin(currentMin + 10)
            }
            MinMaxUpDownState.MaxDown -> if (currentMax >= 10 && (currentMin + 10) < currentMax) {
                setGuideBeamMax(currentMax - 10)
            }
            MinMaxUpDownState.MaxUp -> if (currentMax < MAX_GUIDE_BEAM) {
                setGuideBeamMax(currentMax + 10)
            }
            MinMaxUpDownState.MinLongDown -> if (currentMin >= 100) {
                setGuideBeamMin(currentMin - 100)
            } else if (currentMin >= 10) {
                setGuideBeamMin(currentMin - 10)
            }
            MinMaxUpDownState.MinLongUp -> if (currentMin < (MAX_GUIDE_BEAM - 100) && (currentMin + 100) < currentMax) {
                setGuideBeamMin(currentMin + 100)
            } else if (currentMin < MAX_GUIDE_BEAM && (currentMin + 10) < currentMax) {
                setGuideBeamMin(currentMin + 10)
            }
            MinMaxUpDownState.MaxLongDown -> if (currentMax > 100 && (currentMin + 100) < currentMax) {
                setGuideBeamMax(currentMax - 100)
            } else if (currentMax >= 10 && (currentMin + 10) < currentMax) {
                setGuideBeamMax(currentMax - 10)
            }
            MinMaxUpDownState.MaxLongUp -> if (currentMax < (MAX_GUIDE_BEAM - 100)) {
                setGuideBeamMax(currentMax + 100)
            } else if (currentMax < MAX_GUIDE_BEAM) {
                setGuideBeamMax(currentMax + 10)
            }
        }

        // Save the updated values to preferences
        saveGuideBeamMinToPreference()
        saveGuideBeamMaxToPreference()

        // After updating the state, send the packet
        val newMin = guideBeamMin.value // get the potentially updated value
        val newMax = guideBeamMax.value // get the potentially updated value
        val guideBeam = guideBeam.value.toInt()

        /*
        val value = when(guideBeamValue) {
            0 -> 0
            1 -> newMin
            10 -> newMax
            else -> (newMin + (guideBeamValue - 1) * ((newMax - newMin) / 9))
        }
        */
        val value = (newMin + (guideBeam - 1) * ((newMax - newMin) / 9))

        Timber.d("guideBeam: $value, guideBeam: $guideBeam, guideBeamMax: $newMax, guideBeamMin: $newMin")
        txPacket(READ_WRITE.WRITE, CMD.GUIDE_BEAM, GuideBeam(value = value))
    }

    private val _pulseType = MutableStateFlow(1)    // 0: Single, 1: Repeat
    val pulseType = _pulseType.asStateFlow()
    fun setPulseType(t: Int) {
        _pulseType.value = t
    }

    private val _opTimeHour: MutableStateFlow<Long> = MutableStateFlow(0L)
    val opTimeHour = _opTimeHour.asStateFlow()
    fun setOpTimeHour(h: Long) {
        _opTimeHour.value = h
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // Tx Repeate
    private val _txRepeatCount: MutableStateFlow<Int> = MutableStateFlow(0)
    val txRepeatCount = _txRepeatCount.asStateFlow()
    fun setTxRepeatCount(v: Int) {
        _txRepeatCount.value = v
    }
    fun increaseTxRepeatCount() {
        setTxRepeatCount(txRepeatCount.value + 1)
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // SERIAL - PACKET

    // laserStatus
    private val _laserStatus: MutableStateFlow<LaserStatus> = MutableStateFlow(LaserStatus())
    val laserStatus = _laserStatus.asStateFlow()
    fun setLaserStatus(laserStatus: LaserStatus) {
        _laserStatus.value = laserStatus
    }

    // handPience
    private val _handPiece: MutableStateFlow<HandPiece> = MutableStateFlow(HandPiece())
    val handPiece = _handPiece.asStateFlow()
    fun setHandPiece(handPiece: HandPiece) {
        _handPiece.value = handPiece
    }

    // warning
    private val _warning: MutableStateFlow<Alert> = MutableStateFlow(Alert(0))
    val warning = _warning.asStateFlow()
    fun setWarning(w: Alert) {
        _warning.value = w
    }

    // error
    private val _error: MutableStateFlow<Alert> = MutableStateFlow(Alert(0))
    val error = _error.asStateFlow()
    fun setError(e: Alert){
        _error.value = e
    }

    // version
    private val _version: MutableStateFlow<Version> = MutableStateFlow(Version())
    val version = _version.asStateFlow()
    fun setVersion(v: Version) {
        _version.value = v
    }

    // purge bubble
    private val _purgeBubble: MutableStateFlow<PurgeBubble> = MutableStateFlow(PurgeBubble())
    val purgeBubble: MutableStateFlow<PurgeBubble> = _purgeBubble
    fun setPurgeBubble(p: PurgeBubble) {
        _purgeBubble.value = p
    }

    // dcd gas
    private val _dcdGas: MutableStateFlow<DcdGas> = MutableStateFlow(DcdGas())
    val dcdGas: MutableStateFlow<DcdGas> = _dcdGas
    fun setDcdGas(d: DcdGas) {
        _dcdGas.value = d
    }

    // spray dcd
    private val _sprayDcd: MutableStateFlow<SprayDcd> = MutableStateFlow(SprayDcd())
    val sprayDcd: MutableStateFlow<SprayDcd> = _sprayDcd
    fun setSprayDcd(s: SprayDcd) {
        _sprayDcd.value = s
    }

    // temperature (READ)
    private val _temperature: MutableStateFlow<Temperature> = MutableStateFlow(Temperature())
    val temperature = _temperature.asStateFlow()
    fun setTemperature(t: Temperature) {
        _temperature.value = t
    }

    // temperature (WRITE -  EngineerMode)
    private val _temperature_write: MutableStateFlow<Temperature> = MutableStateFlow(Temperature())
    val temperature_write = _temperature_write.asStateFlow()
    fun setTemperatureWrite(t: Temperature) {
        _temperature_write.value = t
    }

    // oven
    private val _oven: MutableStateFlow<Oven> = MutableStateFlow(Oven())
    val oven: MutableStateFlow<Oven> = _oven
    fun setOven(o: Oven) {
        _oven.value = o
    }

    // q swtich
    private val _qSwitch: MutableStateFlow<QSwitch> = MutableStateFlow(QSwitch())
    val qSwitch: MutableStateFlow<QSwitch> = _qSwitch
    fun setQSwitch(q: QSwitch) {
        _qSwitch.value = q
    }

    // q swtich sub
    private val _qSwitchSub: MutableStateFlow<QSwitch> = MutableStateFlow(QSwitch())
    val qSwitchSub: MutableStateFlow<QSwitch> = _qSwitchSub
    fun setQSwitchSub(q: QSwitch) {
        _qSwitchSub.value = q
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // Energy detect

    // Energy Detect - Refer 2. (Engineer Mode)
    private val _energyDetectRefer2: MutableStateFlow<Refer2> = MutableStateFlow(Refer2())
    val energyDetectRefer2 = _energyDetectRefer2.asStateFlow()
    fun setEnergyDetectRefer2(value: Refer2) {
        _energyDetectRefer2.value = value
    }

    // EnergyVersion (Energy Detect - Version)
    private val _energyVersion: MutableStateFlow<EnergyVersion> = MutableStateFlow(EnergyVersion())
    val energyVersion: MutableStateFlow<EnergyVersion> = _energyVersion
    fun setEnergyVersion(v: EnergyVersion) {
        _energyVersion.value = v
    }

    // EnergyHandpiece (Energy Detect - Check if insert in mount)
    private val _energyHandpiece: MutableStateFlow<EnergyHandpiece> = MutableStateFlow(EnergyHandpiece())
    val energyHandpiece: MutableStateFlow<EnergyHandpiece> = _energyHandpiece
    fun setEnergyHandpiece(h: EnergyHandpiece) {
        _energyHandpiece.value = h
    }

    // EnergyControl (Energy Detect - Control to detect energy)
    private val _energyControl: MutableStateFlow<EnergyControl> = MutableStateFlow(EnergyControl())
    val energyControl: MutableStateFlow<EnergyControl> = _energyControl
    fun setEnergyControl(c: EnergyControl) {
        _energyControl.value = c
    }

    // EnergyMeasured (Energy Detect - Transmit the measured data)
    private val _energyMeasured: MutableStateFlow<EnergyMeasured> = MutableStateFlow(EnergyMeasured())
    val energyMeasured: MutableStateFlow<EnergyMeasured> = _energyMeasured
    fun setEnergyMeasured(m: EnergyMeasured) {
        _energyMeasured.value = m
    }

    // EnergyMeasured Write (Energy Detect - Write value from Engineer Mode)
    private val _energyMeasuredWrite: MutableStateFlow<EnergyMeasured> = MutableStateFlow(EnergyMeasured())
    val energyMeasuredWrite: MutableStateFlow<EnergyMeasured> = _energyMeasuredWrite
    fun setEnergyMeasuredWrite(m: EnergyMeasured) {
        _energyMeasuredWrite.value = m
    }

    // rcvd data
    private var _rcvdData: ByteArray = byteArrayOf()
    
    ////////////////////////////////////////////////////////////////////////////////////////////////
    // Play Beep Sound

    private var mediaPlayer: MediaPlayer? = null
    private var beepJob: Job? = null

    suspend fun playBeepSound() {
        if (beepJob?.isActive == true) {
            Timber.d( "MediaPlayer isActive")
            return
        }

        beepJob = viewModelScope.launch(dispatcherProvider.io) {
            try {
                if (mediaPlayer == null) {
                    mediaPlayer = MediaPlayer.create(applicationContext, R.raw.sound)
                }

                mediaPlayer?.let { player ->
                    Timber.d("MediaPlayer playing")
                    player.seekTo(0) // Start from beginning
                    player.start()

                    val delayMillis = 50L // player.duration.toLong()
                    Timber.d("delayMillis: $delayMillis")

                    delay(delayMillis)
                }
            } catch (e: Exception) {
                Timber.e(e, "MediaPlayer error")
                mediaPlayer?.release()
                mediaPlayer = null
            }
        }

        beepJob?.join()
    }

    override fun onCleared() {
        super.onCleared()
        beepJob?.cancel() // Stop any pending beep delay
        mediaPlayer?.stop()
        mediaPlayer?.release()
        mediaPlayer = null
    }

    fun txPacketOnce() {
        // viewModel init 으로 이동. 필요.
        // 경고 정보 조회 (주기적 heart beat)
        // repeatOnLifecycle은 Activity가 포그라운드에 있을 때로 한정지어, 특정 Lifecycle이 Trigger 되었을 때 동작하도록 만드는 block 임.

        // tx Version Read
        txPacket(READ_WRITE.READ, CMD.VERSION, byteArrayOf(0x41.toByte()))

        // tx Q-Switch Write
        txPacket(READ_WRITE.WRITE, CMD.Q_SWITCH, qSwitch.value)

        // tx Guide Beam Write
        txPacket(READ_WRITE.WRITE, CMD.GUIDE_BEAM, GuideBeam(value = guideBeam.value.toInt()))

        // tx DCD_GAS Write (DEFAULT VALUE)
        txPacket(READ_WRITE.WRITE, CMD.DCD_GAS, dcdGas.value.copy(status = 0x50))

        // tx SPRAY_DCD Write (DEFAULT VALUE)
        txPacket(READ_WRITE.WRITE, CMD.SPRAY_DCD, sprayDcd.value)
    }

    // Example: Emitting an event after a delay
    fun txPacketLoop() {
        viewModelScope.launch {
            // viewModel init 으로 이동. 필요.
            // 경고 정보 조회 (주기적 heart beat)
            // repeatOnLifecycle은 Activity가 포그라운드에 있을 때로 한정지어, 특정 Lifecycle이 Trigger 되었을 때 동작하도록 만드는 block 임.

            while (true) {
                val noOfRepeat = if (showEnergyDetectPopup) {
                    2
                } else {
                    5
                }

                delay(txRepeatMillis)

                /*
                if (error.value.code != 0) {
                    Timber.e("error.value.code: ${error.value.code}")
                    continue
                }
                */

                //  hand piece connection check
                if (txRepeatCount.value % noOfRepeat == 0)
                    txPacket(READ_WRITE.READ, CMD.HAND_PIECE, byteArrayOf())

                // hand piece mount (detector)
                // - Once handpiece is mounted (it receives 0x47 (Good)) don't send again
                // - If Unmounted, F/W send EnergyHandpiece result again.
                else if (txRepeatCount.value % noOfRepeat == 1) {
                    if (energyHandpiece.value.status == 0x47) {
                        Timber.d("Handpiece mount OK.. don't send packet again")
                    } else  {
                        txPacket(
                            READ_WRITE.READ,
                            CMD.ENERGY_DETECT,
                            EnergyHandpiece(type = 0x02, status = 0x41)
                        )
                    }
                }

                //  hp shot count
                else if (txRepeatCount.value % noOfRepeat == 2)
                    txPacket(READ_WRITE.READ, CMD.HP_SHOT_COUNT, byteArrayOf())

                // warning
                else if (txRepeatCount.value % noOfRepeat == 3)
                    txPacket(READ_WRITE.READ, CMD.WARNING, byteArrayOf())

                // temperature
                else if (txRepeatCount.value % noOfRepeat == 4) {
                    txPacket(READ_WRITE.READ, CMD.TEMPERATURE, byteArrayOf())
                }


                // q switch (GUI save : data source)
                // txPacket(READ_WRITE.READ, CMD.Q_SWITCH, byteArrayOf())

                // error (event type)
                // txPacket(READ_WRITE.READ, CMD.ERROR, byteArrayOf())

                // opTime - update system operation time (total uptime in hour)
                updateHourlyUptime()

                // update tx repeat count
                increaseTxRepeatCount()
            }
        }
    }

    private suspend fun updateHourlyUptime() {
        val currentRealTimeMillis = System.currentTimeMillis()
        val elapsedSinceLastCheckMillis = currentRealTimeMillis - lastCheckRealTimeMillis.value

        val newFullHoursPassed = elapsedSinceLastCheckMillis.milliseconds.toLong(DurationUnit.HOURS)
        //Timber.d("Elapsed since last check: ${elapsedSinceLastCheckMillis.milliseconds}, New full hours: $newFullHoursPassed")

        if (newFullHoursPassed > 0) {
            setOpTimeHour( opTimeHour.value + newFullHoursPassed )
            saveOpTimeHourToPreference()
            Timber.d("Updated total uptime: ${opTimeHour.value} hours. Last check updated to: $newFullHoursPassed")

            lastCheckRealTimeMillis.value = currentRealTimeMillis
        } else {
            //Timber.d("Not a full hour passed since last check.")
        }
    }

    private val _isCommunicationStable = MutableStateFlow(true)
    val isCommunicationStable = _isCommunicationStable.asStateFlow()

    private var lastResponseTime = System.currentTimeMillis()

    fun rxPacketLoop() {
        viewModelScope.launch(dispatcherProvider.io) {
            serialPortRepository.open(SERIAL_PORT_NAME, SERIAL_BAUDRATE)
                .catch { e ->
                    // DETECT: Open Fail or Port Crash
                    Timber.e("Serial Port Open Fail: ${e.message}")
                    _isCommunicationStable.value = false
                }.collect {
                    //isLoading.value = true; _isLoadingFlow.value = true;
                    if (it is SerialResult.Success) {
                        _isCommunicationStable.value = true
                        lastResponseTime = System.currentTimeMillis() // Reset timeout

                        // 수신 패킷 데이터 Hex 덤프 (로깅)
                        /*
                        if (BuildConfig.DEBUG) {
                            it.data.printHex("RX")
                        }
                        */

                        // Combine the persistent buffer with the new data
                        val combinedData = _rcvdData + it.data

                        // Find all complete packets and get the leftover data
                        val (foundPackets, remainedData) = findCompletePackets(combinedData)

                        // Update the persistent buffer with the new leftover data for the next cycle
                        _rcvdData = remainedData

                        // Process each complete packet that was found
                        if (foundPackets.isNotEmpty()) {
                            Timber.d("Found ${foundPackets.size} complete packets. Processing...")
                            foundPackets.forEach { packetBytes ->
                                val result: SerialResult<Packet?> = packetBytes.toPacketResult()
                                val packet: Packet? = if (result is SerialResult.Success) result.data else null

                                if (packet != null) {
                                    procRxPacket(packet)
                                } else {
                                    Timber.e("Packet parsing error for:")
                                }
                            }
                        }

                        // Log remaining data if any
                        if (_rcvdData.isNotEmpty()) {
                            Timber.d("Remained data (${_rcvdData.size} bytes) saved for next read.")
                        }
                    } else if (it is SerialResult.Error) {
                        Timber.e("error: %s", it.exception.message)
                        _isCommunicationStable.value = false

                        if (!BuildConfig.DEBUG) {
                            setError(Alert(code = 1)) // PSU/COMM error
                        }

                        //serialPortRepository.close()
                        //delay(5 * 1000L)    // 5 초 간격으로 재오픈 시도
                        //rxPacketLoop()
                    }

                    //isLoading.value = false; _isLoadingFlow.value = false;
                }
        }

        // Start a Watchdog Coroutine to detect "No Response"
        monitorConnectionTimeout()
    }

    private fun monitorConnectionTimeout() {
        viewModelScope.launch(dispatcherProvider.io) {
            while (isActive) {
                val currentTime = System.currentTimeMillis()
                if (currentTime - lastResponseTime > RX_TIMEOUT_THRESHOLD) {
                    if (isCommunicationStable.value) {
                        Timber.w("Serial Timeout: No response for ${RX_TIMEOUT_THRESHOLD}ms")
                        _isCommunicationStable.value = false

                        if (!BuildConfig.DEBUG) {
                            setError(Alert(code = 1)) // PSU/COMM error
                        }
                    }
                }
                delay(500) // Check every half second
            }
        }
    }

    /**
     * Splits a ByteArray into a list of complete packets (from STX to ETX) and leftover data.
     * A packet is defined by STX (0x21) and ETX (0x0d).
     *
     * @param data The combined buffer of old and new data to search through.
     * @return A Pair containing a List of complete packet ByteArrays and the remaining ByteArray.
     */
    private fun findCompletePackets(data: ByteArray): Pair<List<ByteArray>, ByteArray> {
        val packets = mutableListOf<ByteArray>()
        var searchArea = data
        val stx = 0x21.toByte()
        val etx = 0x0d.toByte()

        while (true) {
            val stxIndex = searchArea.indexOf(stx)
            if (stxIndex == -1) {
                // No STX found in the remaining data, so all of it is leftover.
                return Pair(packets, searchArea)
            }

            // Search for ETX *after* the found STX
            val etxIndex = searchArea.drop(stxIndex).indexOf(etx)
            if (etxIndex == -1) {
                // Found an STX but no corresponding ETX. The rest of the data is a partial packet.
                // Keep the data from STX onwards as the remainder.
                val remaining = searchArea.sliceArray(stxIndex..searchArea.lastIndex)
                return Pair(packets, remaining)
            }

            // Found a complete packet. The actual ETX index in the original searchArea.
            val absoluteEtxIndex = stxIndex + etxIndex
            val completePacket = searchArea.sliceArray(stxIndex..absoluteEtxIndex)
            packets.add(completePacket)

            // The next search should start right after the found packet.
            searchArea = searchArea.sliceArray((absoluteEtxIndex + 1)..searchArea.lastIndex)
        }
    }


    private fun procRxPacket(packet: Packet) {
        viewModelScope.launch {
            when ( packet.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG) ) {
                CMD.VERSION -> {
                    val v = packet.data.toVersion()
                    setVersion(v.copy())
                    Timber.d("VERSION: ${v}")
                }
                CMD.LASER_STATUS -> {
                    val l = packet.data.toLaserStatus()
                    setLaserStatus(l.copy())
                    // on receuve laser on (or interval) increase count and save to pref.
                    // Laser On 패킷만 counting 하도록 변경 (2개 패킷 모두 수신 됨)
                    if (_laserStatus.value.laserStatus == LASER_STATUS.LASER_ON /* || l.laserStatus == LASER_STATUS.INTERVAL */) {
                        playBeepSound()

                        increaseLaserCount()
                        increaseHpCount()

                        increaseLampCount()
                        saveLampCountToPreference()

                        val repetitionIndex = repetitionAngle.value.degreeToStep(totalSteps = repetitionList.value.size)
                        val repetition = repetitionList.value[repetitionIndex]
                        Timber.d("repetition: ${repetition}")
                        if (repetition < 6.0f) {
                            increaseDcdCount()
                            saveDcdCountToPreference()
                        }
                    }

                    val hexLaserStatus = String.format("laserStatus: %02X", _laserStatus.value.laserStatus)
                    Timber.d("LASER_STATUS: ${hexLaserStatus}")
                    Timber.d("laserCount: ${laserCount.value}")
                    Timber.d("lampCount: ${lampCount.value}")
                }
                CMD.PURGE_BUBBLE -> {
                    val p = packet.data.toPurgeBubble()
                    setPurgeBubble(p.copy())
                    Timber.d("PURGE_BUBBLE: ${p}")
                }
                CMD.DCD_GAS -> {
                    val d = packet.data.toDcdGas()
                    setDcdGas(d.copy())
                    Timber.d("DCD_GAS: ${d}")

                    /* laser 발진 (on)시 증가 (6Hz 미만시)
                    if (d.status == DCD_STATUS.ON) {  // ox41 == ON
                        ++_dcdCount.value
                        saveDcdCountToPreference()
                    }
                    */
                }
                CMD.SPRAY_DCD -> {
                    val s = packet.data.toSprayDcd()
                    setSprayDcd(s.copy())

                    /*
                    setDcdCount( dcdCount.value + 1 )
                     */
                    Timber.d("SPRAY_DCD: ${s}")
                }
                CMD.HAND_PIECE -> {
                    val h = packet.data.toHandPiece()

                    // check handPiece is changed
                    if (h.type == handPiece.value.type) {
                        Timber.d("handPiece is not changed")
                        return@launch
                    }

                    setHandPiece(h.copy())
                    Timber.d("HAND_PIECE: ${h}")

                    // laser Count 초기화
                    setLaserCount(0)

                    // send STANDBY packet
                    txPacket(READ_WRITE.WRITE, CMD.LASER_STATUS, byteArrayOf(LASER_STATUS.STAND_BY.toByte()))

                    // laser 상태 초기화 (stand by: 0x53)
                    setLaserStatus(laserStatus.value.copy (
                        laserStatus = 0x53
                    ))

                    // init energy detect
                    setEnergyHandpiece(EnergyHandpiece(0x02, 0x00))
                    setEnergyControl(EnergyControl(0x03, 0x00))
                    setEnergyMeasured(EnergyMeasured(0x04, 0x00, 0x00))

                    // hand piece 별 count 증가
                    when (h.type) {
                        1 -> {  // hp 5x5
                            setLifeTime(
                                _lifeTime.value.copy(
                                    hp5x5 = _lifeTime.value.hp5x5 + 1
                                )
                            )
                            saveLifeTimeToPreference()
                            Timber.d("_lifeTime (hp5x5): ${_lifeTime}")
                        }
                        2 -> {
                            setLifeTime(
                                _lifeTime.value.copy(
                                    hp7x7 = _lifeTime.value.hp7x7 + 1
                                )
                            )
                            saveLifeTimeToPreference()
                            Timber.d("_lifeTime (hp7x7): ${_lifeTime}")
                        }
                        3 -> {
                            setLifeTime(
                                _lifeTime.value.copy(
                                    hp10x10 = _lifeTime.value.hp10x10 + 1
                                )
                            )
                            saveLifeTimeToPreference()
                            Timber.d("_lifeTime (hp10x10): ${_lifeTime}")
                        }
                        4 -> {
                            setLifeTime(
                                _lifeTime.value.copy(
                                    hp12x12 = _lifeTime.value.hp12x12 + 1
                                )
                            )
                            saveLifeTimeToPreference()
                            Timber.d("_lifeTime (hp12x12): ${_lifeTime}")
                        }
                        5 -> {
                            setLifeTime(
                                _lifeTime.value.copy(
                                    hp3x15 = _lifeTime.value.hp3x15 + 1
                                )
                            )
                            saveLifeTimeToPreference()
                            Timber.d("_lifeTime (hp3x15): ${_lifeTime}")
                        }
                        else -> {
                            Timber.d("unknwon hand piece type: ${h.type}")
                            return@launch
                        }
                    }

                    // hand piece가 변경 시점 에서, pref 테이블 값을 -> energyTable 로 로딩
                    loadFluenceTable(handPiece.value.type)

                    // parameter angle initialize
                    setFluenceAngle(0f)
                    setRepetitionAngle(0f)
                    setPulseAngle(0f)

                    // fluenceList 초기화 (현재 pulse duration 값에 해당하는 목록)
                    val pulseWidthStep = 0
                    val pulseWidth = PulseDurations[pulseWidthStep] // preset.value.pulseWidth
                    val newFluenceList = energyTable.value.getKey2ListForKey1(pulseWidth)

                    setFluenceList(newFluenceList)
                    Timber.d("newFluenceList: ${newFluenceList}")

                    // repetitionList 초기화.
                    loadHzTable( handPiece.value.type )

                    val hzType = hzTable.value.getValue(pulseWidth, newFluenceList.get(0))
                    Timber.d("hzType: ${hzType}")
                    val list = RepetitionsByColorKey.get(hzType)!!
                    setRepetitionList( list )
                    Timber.d("newRepetitionList: ${_repetitionList.value}")

                    //initialize preset to first value
                    //val fluenceStep = fluenceAngle.value.degreeToStep(totalSteps = fluenceList.value.size)
                    //val newFluenceDegree = fluenceStep.stepToDegree(totalSteps = fluenceList.value.size)
                    //setFluenceAngle( newFluenceDegree )
                }
                CMD.HP_SHOT_COUNT -> {
                    val c = packet.data.toHpShotCount()
                    setHpCount(c.count)

                    saveHpCountToPreference()
                    Timber.d("HP_SHOT_COUNT: ${hpCount.value}")
                }
                CMD.TEMPERATURE -> {
                    val t = packet.data.toTemperature()

                    // DEBUG
                    Timber.d("BEFORE UPDATE:")
                    chartDataQueue.value.toList().sortedBy{ it.timestamp }.takeLast(25).forEachIndexed { idx, log ->
                        Timber.d("[%02d] %s (%s)".format(idx, log.timestamp.toYYYYMMDDHHMMSSString(), log.timestamp.toString()))
                    }

                    // 1. get TemperatureTimeStamp from received temperature value
                    val newTemperaturePoint = TemperatureTimeStamp.fromTemperature(t, System.currentTimeMillis())
                    Timber.d("[VM] ${newTemperaturePoint.timestamp.toYYYYMMDDHHMMSSString()} (${newTemperaturePoint.timestamp})")

                    // 2. Get the current queue value once.
                    val currentQueue = chartDataQueue.value

                    // 3. Create a new queue instance to ensure state is updated.
                    val updatedQueue = FixedSizeLIFOQueue<TemperatureTimeStamp>(MAX_CHART_COLUMN_DATA_SIZE)

                    // 4. Add all items from the current queue to the new one.
                    updatedQueue.addAll(currentQueue.toList())

                    // 5. Add ONLY the single new data point.
                    updatedQueue.add(newTemperaturePoint)

                    // DEBUG
                    Timber.d("UPDATE QUEUE:")
                    updatedQueue.toList().sortedBy{ it.timestamp }.takeLast(25).forEachIndexed { idx, log ->
                        Timber.d("[%02d] %s (%s)".format(idx, log.timestamp.toYYYYMMDDHHMMSSString(), log.timestamp.toString()))
                    }

                    // 6. Update the repository with the new state.
                    setChartDataQueue(updatedQueue)

                    withContext(Dispatchers.Main) {
                        // 7. Update the UI with the new data.
                        setTemperature(t.copy())

                        // 8. Save Temperature to Preference
                        saveTemeratureToPreference()

                        when {
                            temperature.value.ktp > temperature_write.value.ktp -> {
                                Timber.e("temperature.value.ktp > temperature_write.value.ktp")
                                /* #74 - KTP의 Write 값은 설정을 하기 위한 값임 (Error는 PSU에서 패킷을 전송함)
                                if (!showEnergyDetectPopup) {
                                    setError(Alert(code = 10)) // ktp error
                                }
                                */
                            }
                            temperature.value.chamber1 > temperature_write.value.chamber1 -> {
                                Timber.e("temperature.value.chamber1 > temperature_write.value.chamber1")
                                if (!showEnergyDetectPopup) {
                                    setError(Alert(code = 11)) // chamber error
                                }
                            }
                            temperature.value.chamber2 > temperature_write.value.chamber2 -> {
                                Timber.e("temperature.value.chamber2 > temperature_write.value.chamber2")
                                if (!showEnergyDetectPopup) {
                                    setError(Alert(code = 11)) // chamber error
                                }
                            }
                            temperature.value.basePlate > temperature_write.value.basePlate -> {
                                Timber.e("temperature.value.basePlate > temperature_write.value.basePlate")
                                if (!showEnergyDetectPopup) {
                                    setError(Alert(code = 12)) // basePlate error
                                }
                            }
                            temperature.value.water > temperature_write.value.water -> {
                                Timber.e("temperature.value.water > temperature_write.value.water")
                                if (!showEnergyDetectPopup) {
                                    setError(Alert(code = 13)) // water error
                                }
                            }
                        }
                    }

                    Timber.d("chartDataQueue.size: ${chartDataQueue.value.size}")
                    //Timber.d("update chartDataQueue.value: ${chartDataQueue.value}")
                }
                CMD.OVEN -> {
                    val o = packet.data.toOven()
                    setOven(o.copy())
                    Timber.d("OVEN: ${oven.value}")
                }
                CMD.Q_SWITCH -> {
                    val q = packet.data.toQSwitch()
                    setQSwitch(q.copy())
                    Timber.d("Q_SWITCH: ${qSwitch.value}")
                }
                CMD.ENERGY_DETECT -> {
                    val type = packet.data.sliceArray(0..0).toInt()
                    Timber.d("ENERGY_DETECT: ${type}")
                    when (type) {
                        0x01 -> {
                            val v = packet.data.toEnergyVersion()
                            setEnergyVersion(v.copy())
                            Timber.d("EnergyVersion: ${v}")
                        }

                        0x02 -> {
                            val h = packet.data.toEnergyHandpiece()
                            setEnergyHandpiece(h.copy())

                            // init energy control & measured
                            setEnergyControl(EnergyControl(0x03, 0x00))
                            setEnergyMeasured(EnergyMeasured(0x04, 0x00, 0x00))

                            Timber.d("EnergyHandpiece: ${h}")
                        }

                        0x03 -> {
                            val c = packet.data.toEnergyControl()
                            setEnergyControl(c.copy())

                            // init energy measured
                            setEnergyMeasured(EnergyMeasured(0x04, 0x00, 0x00))

                            Timber.d("EnergyControl: ${c}")
                        }

                        0x04 -> {
                            val m = packet.data.toEnergyMeasured()
                            setEnergyMeasured(m.copy())

                            if (energyMeasured.value.measured < energyMeasuredWrite.value.measured * energyDetectRefer2.value.good) {
                                // Send back Good
                                txPacket(
                                    rw = READ_WRITE.WRITE,
                                    cmd = CMD.ENERGY_DETECT,
                                    data = EnergyMeasured(
                                        type = 0x04,
                                        status = 0x47,  // 'G': Good
                                    )
                                )

                            } else {
                                // Send Not Good
                                txPacket(
                                    rw = READ_WRITE.WRITE,
                                    cmd = CMD.ENERGY_DETECT,
                                    data = EnergyMeasured(
                                        type = 0x04,
                                        status = 0x4E,  // 'N': Not Good
                                    )
                                )
                            }
                            Timber.d("EnergyMeasured: ${m}")
                        }

                        else -> { // error }
                        }
                    }
                }
                CMD.WARNING -> {
                    val w = packet.data.toWarning()
                    setWarning(w.copy())
                    Timber.d("WARNING: ${warning.value}")
                }
                CMD.ERROR -> {
                    val e = packet.data.toError()
                    setError(e.copy())
                    Timber.d("ERROR: ${e}")
                }
            }

            // 3) add to serial log (memory)
            //serialLogs.add(it.data.getHexString(TX_RX.RX))

            // 4) add to serial log (db)
            when ( packet.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG) ) {
                CMD.LASER_STATUS, CMD.TEMPERATURE, CMD.WARNING, CMD.ERROR -> {
                    val logToInsert = SerialLog(
                        ts = System.currentTimeMillis(),
                        cmd = packet.cmd.toInt().mask(CMD_FLAG)
                            .mask(WRITE_FLAG),
                        tx = false,
                        w = (packet.cmd.toInt().mask(WRITE_FLAG) != 0),
                        data = packet.toByteArray(),
                        desc = packet.toByteArray().getHexString()
                    )
                    Timber.d("Attempting to insert SerialLog: $logToInsert")
                    withContext(Dispatchers.IO) {
                        try {
                            databaseRepository.insertSerialLog(serialLog = logToInsert)
                            Timber.d("SerialLog insertion successful.")
                        } catch (e: Exception) {
                            Timber.e(e, "Error inserting SerialLog")
                        }
                    }
                }
            }

            if (BuildConfig.DEBUG) {
                val rwStr = if (packet.cmd.toInt().mask(WRITE_FLAG) != 0) "W" else "R"
                val cmdStr = getCmdString(packet.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG))
                packet.toByteArray().printHex("RX", rwStr, cmdStr)
            }
        }
    }

    fun txLaserStatusEntry(laserStatus: Int) {
        viewModelScope.launch {
            // 1. Main > Gauge : get P(ms), F(J/cm²)
            val pulseWidthStep = pulseAngle.value.degreeToStep(totalSteps = PulseDurations.size)
            val pulseWidth = PulseDurations[pulseWidthStep] // preset.value.pulseWidth

            val fluenceStep = fluenceAngle.value.degreeToStep(totalSteps = fluenceList.value.size)
            val fluence = fluenceList.value.get(fluenceStep) //preset.value.fluence

            val repetitionStep = repetitionAngle.value.degreeToStep(totalSteps = repetitionList.value.size)
            val repetition = if (pulseType.value == 0) {
                0.0f
            } else {
                repetitionList.value.get(repetitionStep)
            }
            Timber.d("pulseWidth: ${pulseWidth}, fluence: ${fluence}, repetition: ${repetition}")

            // 2. Energy Table : get energy(J)
            val energy = energyTable.value[Pair(pulseWidth, fluence)] ?: 0f

            if (energy == 0f) {
                Timber.e("energy == 0")
                return@launch
            }

            // eg. pulseWidth: 0.5, fluence: 2.2 -> energy: 0.55
            Timber.d("pulseWidth: ${pulseWidth}, fluence: ${fluence} -> energy: ${energy}")

            // 2. get voltage from Voltage Table with (J)
            val v = calculateInterpolatedC(pulseWidth, energy)
            Timber.d("v: ${v}")

            /*
            Toast.makeText(applicationContext,
                "(ms: %.1f, J/cm²: %.1f) -> (J: %.3f, V: %.3f)".format(
                    pulseWidth, fluence, energy, v
                ),
                Toast.LENGTH_SHORT
            ).show()
            */

            Timber.d(
                "(ms: %.1f, J/cm²: %.1f) -> (J: %.3f, V: %.3f)".format(
                    pulseWidth, fluence, energy, v
                )
            )

            // 3. conver it to Laser Status
            val laserStatusPacket = LaserStatus(
                laserStatus = laserStatus,
                voltage = v.toInt(),
                onTime = pulseWidth,
                frequence = repetition // preset.value.repetition,
            )

            setLaserStatus(laserStatusPacket)
            
            txPacket(READ_WRITE.WRITE, CMD.LASER_STATUS, laserStatusPacket)
        }
    }

    private fun calculateInterpolatedC(pulseWidth: Float, energy: Float): Float {
        // 1. Find the entries in the map where the first element of the key (first) matches the given pulseWidth
        //    from VoltageTable (eg. 0.5f --> listOf(Pair(0.5f,0.5f), Pair(0.5f,1.5f), ...))
        //    where, volatageTable is Map<Pair<PulseWidth(ms), Energy(J)>, Voltage(V)>
        val v = voltageTable.value
        val PulseWidthrelevantEntries = v.filterKeys { it.first == pulseWidth }
        Timber.d("PulseWidthrelevantEntries : $PulseWidthrelevantEntries")
        /*
        // eg. pulseWidth: 0.5, energy: 0.55
        // Map<Pair<PulseWidth(ms), Energy(J)>, Voltage(V)>
        Pair(0.5f,0.5f) to 440,
        Pair(0.5f,1.5f) to 450,
        Pair(0.5f,3f) to 460,
        Pair(0.5f,4f) to 470,
        Pair(0.5f,5f) to 480,
        Pair(0.5f,6f) to 490,
        Pair(0.5f,7f) to 500,
        Pair(0.5f,8f) to 510,
        Pair(0.5f,9f) to 520,
        Pair(0.5f,10f) to 530,
        Pair(0.5f,11f) to 540,
        Pair(0.5f,12f) to 550,
        */

        // If no entries match 'pulseWidth', return 0 (its error should match at )
        if (PulseWidthrelevantEntries.isEmpty()) {
            Timber.e("No entries found for pulseWidth = $pulseWidth")
            return 0f
        }

        // 2. Extract the (Energy(J), Voltage(V)) pairs from the relevant entries and sort them by first (pulseWidth)
        val EnergyAndVoltages = PulseWidthrelevantEntries.map {
            it.key.second to it.value.value.toFloat()
        }.sortedBy { it.first }
        Timber.d("EnergyAndVoltages : $EnergyAndVoltages")

        /*
        // eg. pulseWidth: 0.5, energy: 0.55
        // Map<Energy(J), Voltage(V)>
            0.5f to 440,
                        * <--- here
            1.5f to 450,
              3f to 460,
              4f to 470,
              5f to 480,
              6f to 490,
              7f to 500,
              8f to 510,
              9f to 520,
             10f to 530,
             11f to 540,
             12f to 550,
        */

        // 3. Find the first value (Energy(J)) same as given energy E(J)
        val indexOfEnegrySameAsFirst = EnergyAndVoltages.indexOfFirst { it.first == energy }
        if (indexOfEnegrySameAsFirst != -1) {
            val (_: Float, v: Float) = EnergyAndVoltages[indexOfEnegrySameAsFirst]
            Timber.d("Same Energy Value: return voltage of it ${v}")
            return v
        }

        // 4. else Find the first value (Energy(J)) greater than given energy E(J)
        val indexOfEnegryGreaterFirst = EnergyAndVoltages.indexOfFirst { it.first > energy }
        Timber.d("indexOfEnegryGreaterFirst = $indexOfEnegryGreaterFirst")  // should be 1 in eg. pulseWidth: 0.5, energy: 0.55

        // If no E(J) value is greater than given energy, it means given-energy is beyond the range of available E(J) values.
        // should match any one so its error case.
        if (indexOfEnegryGreaterFirst == -1) {
            Timber.e("No E(J) found from VoltageTable for given energy (from Main> FluenceTable) = $energy")
            return 0f
        }

        // Get (e: Energy(J), v: Voltage(V))
        val (e2: Float, v2: Float) = EnergyAndVoltages[indexOfEnegryGreaterFirst]
        val (e1: Float, v1: Float) = EnergyAndVoltages[indexOfEnegryGreaterFirst - 1]

        Timber.d("e1: ${e1}, e2: ${e2}")
        Timber.d("v1: ${v1}, v2: ${v2}")

        // Calculate the interpolated Energy value using linear interpolation
        // eg. pulseWidth: 0.5, energy: 0.55
        val interpolatedC = v1 + (v2 - v1) * ((energy - e1) / (e2 - e1))
        Timber.d("interpolatedC: ${interpolatedC}")

        return interpolatedC
    }

    fun <T> txPacket(rw: READ_WRITE, cmd: Int, data: T) {
        viewModelScope.launch {
            var txPacket = byteArrayOf()
            var dataArray = byteArrayOf()

            //isLoading.value = true; _isLoadingFlow.value = true;

            if (rw == READ_WRITE.READ) {
                //dataArray = if (cmd == CMD.VERSION || cmd == CMD.LASER_STATUS) byteArrayOf(0x41) else byteArrayOf()
                dataArray = when(cmd) {
                    CMD.VERSION, CMD.LASER_STATUS -> byteArrayOf(0x41)
                    CMD.ENERGY_DETECT -> when(data) {
                        is EnergyVersion -> byteArrayOf(0x01)               // handpiece version (R)
                        is EnergyHandpiece -> byteArrayOf(0x02, 0x41)       // Check if insert in mount (R)
                        //is EnergyHandpieceRemove -> byteArrayOf(0x02, 0x42) // remove (W)
                        //is EnergyControl -> byteArrayOf(0x03, 0x41)         // Start (W)
                        //is EnergyControlStop -> byteArrayOf(0x03, 0x42)     // Stop (W)
                        //is EnergyMeasured -> byteArrayOf(0x04, 0x45)        // measured (W)
                        else -> byteArrayOf()
                    }
                    else -> byteArrayOf()
                }
                txPacket = Packet(
                    stx = byteArrayOf(CMD.STX.toByte()),
                    cmd = byteArrayOf(cmd.toByte().or(CMD_FLAG.toByte())),
                    data = dataArray,
                    etx = byteArrayOf(CMD.ETX.toByte())
                ).toByteArray()
            } else {
                dataArray = when(data) {
                    //is EnergyVersion -> byteArrayOf(0x01)               // handpiece version (R)
                    //is EnergyHandpiece -> byteArrayOf(0x02, 0x41)       // Check if insert in mount (R)
                    is EnergyHandpieceRemove -> byteArrayOf(0x02, 0x42) // remove (W)
                    is EnergyControl -> data.toByteArray()              //byteArrayOf(0x03, 0x41)         // Start (W)
                    is EnergyControlStop -> byteArrayOf(0x03, 0x42)     // Stop (W)
                    is EnergyMeasured -> byteArrayOf(0x04, 0x45)        // measured (W)
                    is LaserStatus -> data.toByteArray()
                    is PurgeBubble -> data.toByteArray()
                    is DcdGas -> data.toByteArray()
                    is DcdGasOnOff -> data.toByteArray()
                    is SprayDcd -> data.toByteArray()
                    is GuideBeam -> data.toByteArray()
                    is Oven -> data.toByteArray()
                    is QSwitch -> data.toByteArray()
                    is ByteArray -> data
                    is String -> data.toByteArray()
                    is Int -> data.toString().toByteArray()
                    is Byte -> byteArrayOf(data)
                    else -> byteArrayOf()
                }

                Timber.d("dataArray: ${dataArray}")

                txPacket = Packet(
                    stx = byteArrayOf(CMD.STX.toByte()),
                    cmd = byteArrayOf(
                        cmd.toByte().or(CMD_FLAG.toByte()).or(WRITE_FLAG.toByte())
                    ),
                    data = dataArray,
                    cs = (byteArrayOf(
                        cmd.toByte().or(CMD_FLAG.toByte()).or(WRITE_FLAG.toByte())
                    ) + dataArray).toCS(),
                    etx = byteArrayOf(CMD.ETX.toByte())
                ).toByteArray()
            }

            if (BuildConfig.DEBUG) {
                val rwStr = if (rw == READ_WRITE.READ) "R" else "W"
                val cmdStr = getCmdString(cmd)
                txPacket.printHex("TX", rwStr, cmdStr)
            }

            //serialLogs.add(txPacket.getHexString(TX_RX.TX))

            serialPortRepository.write(txPacket)
        }
    }

    fun close() {
        serialPortRepository.close()
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Preference

    suspend fun saveHpCountToPreference() {
        preferenceRepository.saveHpCountToPreference(hpCount.value)
    }

    suspend fun loadHpCountFromPreference() {
        val hpCountValue = preferenceRepository.getHpCountToPreference().first()
        Timber.d("loadHpCountFromPreference: hpCountValue: ${hpCountValue}")
        setHpCount(hpCountValue)
    }

    suspend fun saveLampCountToPreference() {
        preferenceRepository.saveLampCountToPreference(lampCount.value)
    }

    suspend fun loadLampCountFromPreference() {
        val v = preferenceRepository.getLampCountFromPreference().first()
        Timber.d("loadLampCountFromPreference: v: ${v}")
        setLampCount(v)
    }

    suspend fun saveDcdCountToPreference() {
        preferenceRepository.saveDcdCountToPreference(dcdCount.value)
    }

    suspend fun loadDcdCountFromPreference() {
        val v = preferenceRepository.getDcdCountFromPreference().first()
        Timber.d("loadDcdCountFromPreference: v: ${v}")
        setDcdCount(v)
    }


    suspend fun saveSliderVolumeToPreference() {
        Timber.d("saveSliderVolumeToPreference: sliderVolume: ${sliderVolume.value}")
        preferenceRepository.saveSliderVolumeToPreference(
            sliderVolume.value
        )
    }

    suspend fun initSliderVolumeFromPreference() {
        val sliderVolume = preferenceRepository.getSliderVolumeFromPreference().first()
        Timber.d("loadSliderVolumeFromPreference: sliderVolume: ${sliderVolume}")
        setSliderVolume(sliderVolume)

        /*
        withContext(Dispatchers.Main) {
            setCurrentSystemVolume(
                applicationContext,
                AudioManager.STREAM_MUSIC,  //AudioManager.STREAM_SYSTEM,
                sliderVolume.toInt()
            )
        }
        */
    }

    suspend fun trimSerialLog() {
        withContext(dispatcherProvider.io) {
            try {
                databaseRepository.trimLogs(limit = MAX_LOG_COUNT)
                Timber.d("trimSerialLog successful.")
            } catch (e: Exception) {
                Timber.e(e, "trimSerialLog SerialLog")
            }
        }
    }

    suspend fun saveGuideBeamToPreference() {
        preferenceRepository.saveGuideBeamToPreference(guideBeam.value)
    }

    suspend fun loadGuideBeamFromPreference() {
        val guideBeam = preferenceRepository.getGuideBeamFromPreference().first()
        Timber.d("loadGuideBeamFromPreference: guideBeam: ${guideBeam}")
        setGuideBeam(guideBeam)
    }

    suspend fun saveGuideBeamMinToPreference() {
        preferenceRepository.saveGuideBeamMinToPreference(guideBeamMin.value)
    }

    suspend fun loadGuideBeamMinFromPreference() {
        val guideBeamMin = preferenceRepository.getGuideBeamMinFromPreference().first()
        Timber.d("loadGuideBeamMinFromPreference: v: setGuideBeamMin($guideBeamMin)")
        setGuideBeamMin(guideBeamMin)
    }

    suspend fun saveGuideBeamMaxToPreference() {
        preferenceRepository.saveGuideBeamMaxToPreference(guideBeamMax.value)
    }
    suspend fun loadGuideBeamMaxFromPreference() {
        val guideBeamMax = preferenceRepository.getGuideBeamMaxFromPreference().first()
        Timber.d("loadGuideBeamMaxFromPreference: v: setGuideBeamMax($guideBeamMax)")
        setGuideBeamMax(guideBeamMax)
    }

    suspend fun saveSprayDcdToPreference() {
        preferenceRepository.saveSprayDcdToPreference(sprayDcd.value)
    }

    suspend fun loadSprayDcdFromPreference() {
        val sprayDcd = preferenceRepository.getSprayDcdFromPreference().first()
        Timber.d("loadSprayDcdFromPreference: v: setSprayDcd($sprayDcd)")
        setSprayDcd(sprayDcd)
    }

    suspend fun saveQSwitchToPreference() {
        preferenceRepository.saveQSwitchToPreference(qSwitch.value)
    }
    suspend fun loadQSwitchFromPreference() {
        val qSwitch = preferenceRepository.getQSwitchFromPreference().first()
        Timber.d("loadQSwitchFromPreference: v: setQSwitch($qSwitch)")
        setQSwitch(qSwitch)
    }

    suspend fun saveOpTimeHourToPreference() {
        preferenceRepository.saveOpTimeHourToPreference(opTimeHour.value)
    }

    suspend fun loadOpTimeHourFromPreference() {
        val opTimeHourValue = preferenceRepository.getOpTimeHourFromPreference().first()
        Timber.d("loadOpTimeHourFromPreference: v: setOpTimeHour($opTimeHourValue)")
        setOpTimeHour(opTimeHourValue)
    }

    suspend fun saveLifeTimeToPreference() {
        preferenceRepository.saveLifeTimeToPreference(lifeTime.value)
    }

    suspend fun loadLifeTimeFromPreference() {
        val lifeTime = preferenceRepository.getLifeTimeFromPreference().first()
        Timber.d("loadLifeTimeFromPreference: v: setLifeTime($lifeTime)")
        setLifeTime(lifeTime)
    }

    suspend fun saveTemeratureToPreference() {
        preferenceRepository.saveTemperatureToPreference(temperature.value)
    }

    suspend fun loadTemeratureFromPreference() {
        val temperatureValue = preferenceRepository.getTemperatureFromPreference().first()
        Timber.d("loadTemeratureFromPreference: v: setTemperature($temperatureValue)")
        setTemperature(temperatureValue)
    }

    suspend fun saveTemeratureWriteToPreference() {
        preferenceRepository.saveTemperatureWriteToPreference(temperature_write.value)
    }

    suspend fun loadTemeratureWriteFromPreference() {
        val temperatureWriteValue = preferenceRepository.getTemperatureWriteFromPreference().first()
        Timber.d("loadTemeratureWriteFromPreference: v: setTemperatureWrite($temperatureWriteValue)")
        setTemperatureWrite(temperatureWriteValue)
    }

    suspend fun saveEnergyMeasuredWriteToPreference() {
        preferenceRepository.saveEnergyMeasuredWriteToPreference(energyMeasuredWrite.value)
    }
    suspend fun loadEnergyMeasuredWriteFromPreference() {
        val energyMeasuredWrite = preferenceRepository.getEnergyMeasuredWriteFromPreference().first()
        Timber.d("loadEnergyMeasuredWriteFromPreference: v: setEnergyMeasuredWrite($energyMeasuredWrite)")
        setEnergyMeasuredWrite(energyMeasuredWrite)
    }

    suspend fun saveEnergyRefer2ToPreference() {
        preferenceRepository.saveEnergyRefer2ToPreference(energyDetectRefer2.value)
    }
    suspend fun loadEnergyRefer2FromPreference() {
        val energyDetectRefer2 = preferenceRepository.getEnergyRefer2FromPreference().first()
        Timber.d("loadEnergyRefer2FromPreference: v: setEnergyDetectRefer2($energyDetectRefer2)")
        setEnergyDetectRefer2(energyDetectRefer2)
    }

    suspend fun saveProductSerialListToPreference() {
        preferenceRepository.saveProductSerialListToPreference(productSerialList.value)
    }
    suspend fun loadProductSerialListFromPreference() {
        val productSerialList = preferenceRepository.getProductSerialListFromPreference().first()
        Timber.d("loadProductSerialListFromPreference: v: setProductSerialList($productSerialList)")
        setProductSerialList(productSerialList)
    }

    suspend fun savePowerSupplySerialListToPreference() {
        preferenceRepository.savePowerSupplySerialListToPreference(powerSupplySerialList.value)
    }
    suspend fun loadPowerSupplySerialListFromPreference() {
        val powerSupplySerialList = preferenceRepository.getPowerSupplySerialListFromPreference().first()
        Timber.d("loadPowerSupplySerialListFromPreference: v: setPowerSupplySerialList($powerSupplySerialList)")
        setPowerSupplySerialList(powerSupplySerialList)
    }

    suspend fun saveLaserHandSerialListToPreference() {
        preferenceRepository.saveLaserHandSerialListToPreference(laserHandSerialList.value)
    }
    suspend fun loadLaserHandSerialListFromPreference() {
        val laserHandSerialList = preferenceRepository.getLaserHandSerialListFromPreference().first()
        Timber.d("loadLaserHandSerialListFromPreference: v: setLaserHandSerialList($laserHandSerialList)")
        setLaserHandSerialList(laserHandSerialList)
    }

    suspend fun savePresetListToPreference() {
        preferenceRepository.savePresetListToPreference(
            presetList.value
        )
    }

    suspend fun loadSprayDcdIndexFromPreference() {
        val index = preferenceRepository.getSprayDcdIndexFromPreference().first()
        Timber.d("loadSprayDcdIndexFromPreference -> selectedSprayDcdIndex: $index")
        setSelectedSprayDcdIndex(index)
    }

    suspend fun saveSprayDcdIndexToPreference() {
        preferenceRepository.saveSprayDcdIndexToPreference(
            selectedSprayDcdIndex.value
        )
    }

    suspend fun loadPresetListFromPreference() {
        val presets = preferenceRepository.getPresetListFromPreference().first()
        Timber.d("loadPresetListFromPreference: v: setPresetList($presets)")
        setPresetList(presets)
    }

    suspend fun saveSprayDcdListToPreference() {
        preferenceRepository.saveSprayDcdList(
            sprayDcdList.toMutableStateList()
        )
    }

    suspend fun loadSprayDcdListFromPreference() {
        val sprayDcdOptionListVal = preferenceRepository.getSprayDcdList().first()
        Timber.d("loadSprayDcdListFromPreference: v: setSprayDcdList($sprayDcdOptionListVal)")
        sprayDcdList = sprayDcdOptionListVal.toMutableStateList()
    }

    suspend fun loadTemperatureDataFromDatabase() {
        // Launch this on a background thread from the start
        Timber.d("Executing loadTemperatureDataFromDatabase... (ONCE)")

        // Use .first() to read the data only once ---
        withContext(dispatcherProvider.io) {
            try {
                val logs = databaseRepository.selectSerialLog(100).first()

                // All the processing now happens on this single, initial list of logs.
                val temperatureLogs = logs.filter {
                    it.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG) == CMD.TEMPERATURE
                }

                val distinctTemperatureLogs = temperatureLogs.distinctBy {
                    it.ts / 1000
                }.sortedBy {
                    it.ts
                }.takeLast(MAX_CHART_COLUMN_DATA_SIZE)

                val historicalTemperatures = arrayListOf<TemperatureTimeStamp>()
                distinctTemperatureLogs.forEachIndexed { idx, log ->
                    val t = log.data.toTemperature()
                    val timestamp = log.ts
                    Timber.d(
                        "[%02d] %s (%s)".format(
                            idx,
                            timestamp.toYYYYMMDDHHMMSSString(),
                            timestamp.toString()
                        )
                    )

                    val temperatureTimeStamp =
                        TemperatureTimeStamp.fromTemperature(t, timestamp)
                    historicalTemperatures.add(
                        temperatureTimeStamp
                    )
                }

                // DEBUG
                historicalTemperatures.takeLast(25).forEachIndexed { idx, log ->
                    Timber.d(
                        "[%02d] %s (%s)".format(
                            idx,
                            log.timestamp.toYYYYMMDDHHMMSSString(),
                            log.timestamp.toString()
                        )
                    )
                }

                // Switch to the main thread to update the StateFlow safely
                withContext(dispatcherProvider.main) {
                    addChartDataListQueue(historicalTemperatures)

                    // DEBUG
                    chartDataQueue.value.toList().takeLast(25).forEachIndexed { idx, log ->
                        Timber.d(
                            "[%02d] %s (%s)".format(
                                idx,
                                log.timestamp.toYYYYMMDDHHMMSSString(),
                                log.timestamp.toString()
                            )
                        )
                    }
                }

                Timber.d("...Finished loading ${historicalTemperatures.size} historical temperature points into the queue.")
            } catch (e: Exception) {
                Timber.e("loadTemperatureDataFromDatabase -> %s", e.message)
            }
        }
    }

    fun loadFluenceTable(handPieceType: Int) {
        Timber.d("loadFluenceTable($handPieceType)")

        val energyTable = when (handPieceType) {
            2 -> EnergyTable_7_7
            3 -> EnergyTable_10_10
            4 -> EnergyTable_12_12
            5 -> EnergyTable_3_15
            else -> EnergyTable_5_5
        }

        setEnergyTable( energyTable.mapValues { (_, fValue) -> fValue }.toMutableMap() )
    }

    fun loadHzTable(handPieceType: Int) {
        Timber.d("loadHzTable($handPieceType)")

        val hzTable = when (handPieceType) {
            2 -> HzTable_7_7
            3 -> HzTable_10_10
            4 -> HzTable_12_12
            5 -> HzTable_3_15
            else -> HzTable_5_5
        }

        setHzTable( hzTable.mapValues { (_, fValue) -> fValue }.toMutableMap() )
    }

    /**
     * loadVoltageTableFromPreference (Skip - handPiece 변경 시점에서 해당 테이블 로딩)
     * handPience 각각 Preference에 저장된 테이블 (Engineer) 설정 값을 로딩 함
     */
    suspend fun loadVoltageTableFromPreference() {
        val v = preferenceRepository.getVoltageTableFromPreference().first()
        val voltageTableMap = voltageTable.value
        Timber.d("MM: loadVoltageTableFromPreference() begin")
         v.forEach { (key, value) ->
            val voltageTableVolFlow = voltageTableMap[key]
            if (voltageTableVolFlow != null) {
                voltageTableMap.setVoltageTableValue(key, value)

                //Timber.d("Eng: loadVoltageTableFromPreference() key: $key, value: $value")
            }
        }
        Timber.d("MM: loadVoltageTableFromPreference() finish")
    }

    suspend fun fetchSerialLog() {
        Timber.d(".")
        val logs = databaseRepository.selectSerialLog(100)
            .flowOn(dispatcherProvider.io)
            .catch { e ->
                Timber.e("fetchSerialLog -> %s", e.message)
            }.first()

        Timber.d("fetchSerialLog: ${logs}")
        setSerialLogList(logs)
        Timber.d("..")
    }

    fun applyPreset(priority: Int) {
        val preset = presetList.value.firstOrNull { it.handPieceType == handPiece.value.type && it.priority == priority }
        Timber.d("preset: ${preset}")

        val newPreset = if (preset == null) {
            Preset(
                handPieceType = handPiece.value.type,
                priority = priority,
                fluence = energyTable.value.getKey2ListForKey1(0.5f).first(),
                repetition = repetitionList.value.first(),
                pulseWidth = PulseDurations.first(),
            )
        } else {
            preset
        }

        val fluenceList = energyTable.value.getKey2ListForKey1(newPreset.pulseWidth)

        val pulseStep = PulseDurations.indexOf(newPreset.pulseWidth).takeIf {it != -1} ?: 0
        val fluenceStep = fluenceList.indexOf(newPreset.fluence).takeIf {it != -1} ?: 0
        val repetitionStep = repetitionList.value.indexOf(newPreset.repetition).takeIf {it != -1} ?: 0

        Timber.d("pulseStep: ${pulseStep} fluenceStep: ${fluenceStep} repetitionStep: ${repetitionStep}")

        val pulseAngle = pulseStep.stepToDegree(totalSteps = PulseDurations.size)
        val fluenceAngle = fluenceStep.stepToDegree(totalSteps = fluenceList.size)
        val repetitionAngle = repetitionStep.stepToDegree(totalSteps = repetitionList.value.size)

        Timber.d("pulseStep: ${pulseStep} fluenceStep: ${fluenceStep} repetitionStep: ${repetitionStep}")

        setPulseAngle( pulseAngle )
        setFluenceAngle( fluenceAngle )
        setRepetitionAngle( repetitionAngle )

        Timber.d("pulseAngle: ${pulseAngle}, fluenceAngle: ${fluenceAngle}, repetitionAngle: ${repetitionAngle}")
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // Laser Parameter Control
    ////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Centralized helper to update dependent laser parameters (Fluence list, Repetition list)
     * when a primary parameter (like Pulse Duration or Fluence) changes.
     */
    private fun updateLaserParameters(newPulseDuration: Float, newFluence: Float) {
        // 1. Get the current repetition value BEFORE we change anything.
        val repetitionStep = repetitionAngle.value.degreeToStep(totalSteps = repetitionList.value.size)
        val currentRepetitionValue = repetitionList.value.getOrNull(repetitionStep)

        // 2. Update Fluence List based on the new Pulse Duration
        val newFluenceList = energyTable.value.getKey2ListForKey1(newPulseDuration)
        if (newFluenceList != fluenceList.value) {
            setFluenceList(newFluenceList)
        }

        // 3. Safely Update Repetition List (Prevents NullPointerException)
        val newHzType = hzTable.value.getValue(newPulseDuration, newFluence)
        val newRepetitionList = RepetitionsByColorKey[newHzType] ?: RepetitionsByColorKey[KEY_YELLOW]!!
        if (newRepetitionList != repetitionList.value) {
            setRepetitionList(newRepetitionList)
        }

        // Smartly preserve Repetition Angle
        // 4. Check if the old repetition value exists in the new list.
        val oldRepetitionIndex = if (currentRepetitionValue != null) {
            newRepetitionList.indexOf(currentRepetitionValue)
        } else {
            -1
        }

        if (oldRepetitionIndex != -1) {
            // If the old value exists, set the slider to that position.
            val preservedAngle = oldRepetitionIndex.stepToDegree(totalSteps = newRepetitionList.size)
            setRepetitionAngle(preservedAngle)
            Timber.d("Repetition value $currentRepetitionValue preserved at new angle $preservedAngle.")
        } else {
            // If it doesn't exist, THEN reset the slider to the beginning.
            setRepetitionAngle(0f)
            Timber.d("Repetition value $currentRepetitionValue not supported in new list. Resetting angle.")
        }

        // 5. Conditionally reset the fluence angle slider if needed
        /*
        if (shouldResetFluenceAngle) {
            setFluenceAngle(0f)
        }
        */

        // 6. Any change invalidates the current preset selection.
        setSelectedPresetIndex(0)

        Timber.d("Updated Laser Parameters: pulse=$newPulseDuration, fluence=$newFluence -> newRepListSize=${newRepetitionList.size}")
    }

    /**
     * This is the single entry point for the Up/Down buttons on the main screen's sliders.
     * It correctly dispatches to the specific handlers for each parameter type.
     */
    fun onUpDownButtonClicked(type: LaserParameter, state: UpDownState) {
        when(type) {
            LaserParameter.PulseWidth -> onClickPulseDuration(state)
            LaserParameter.Fluence -> onClickFluence(state)
            LaserParameter.Repetition -> onClickRepetition(state)
        }
    }

    fun onChangePulseDuration(angle: Float) {
        val newPulseStep = angle.degreeToStep(totalSteps = PulseDurations.size)
        setPulseAngle(newPulseStep.stepToDegree(totalSteps = PulseDurations.size))

        val newPulseDuration = PulseDurations[newPulseStep]
        // When pulse duration changes via slider, we use the first available fluence for the new list.
        val firstFluence = energyTable.value.getKey2ListForKey1(newPulseDuration).firstOrNull() ?: 0f

        // Call the centralized helper, resetting the fluence slider.
        updateLaserParameters(
            newPulseDuration = newPulseDuration,
            newFluence = firstFluence,
        )
    }

    fun onClickPulseDuration(state: UpDownState) {
        val currentStep = pulseAngle.value.degreeToStep(totalSteps = PulseDurations.size)
        val newStep = if (state == UpDownState.Up) currentStep + 1 else currentStep - 1

        if (newStep in PulseDurations.indices) {
            setPulseAngle(newStep.stepToDegree(totalSteps = PulseDurations.size))

            val newPulseDuration = PulseDurations[newStep]
            val firstFluence = energyTable.value.getKey2ListForKey1(newPulseDuration).firstOrNull() ?: 0f

            // Call the centralized helper, resetting the fluence slider.
            updateLaserParameters(
                newPulseDuration = newPulseDuration,
                newFluence = firstFluence,
            )
        }
    }

    fun onChangeFluence(angle: Float) {
        setFluenceAngle(angle)

        val pulseStep = pulseAngle.value.degreeToStep(totalSteps = PulseDurations.size)
        val currentPulseDuration = PulseDurations[pulseStep]

        val fluenceStep = angle.degreeToStep(totalSteps = fluenceList.value.size)
        if (fluenceStep in fluenceList.value.indices) {
            val newFluence = fluenceList.value[fluenceStep]
            // Call the helper without resetting the fluence angle, as it's the one being changed.
            updateLaserParameters(
                newPulseDuration = currentPulseDuration,
                newFluence = newFluence,
            )
        }
    }

    fun onClickFluence(state: UpDownState) {
        val currentStep = fluenceAngle.value.degreeToStep(totalSteps = fluenceList.value.size)
        val newStep = if (state == UpDownState.Up) currentStep + 1 else currentStep - 1

        if (newStep in fluenceList.value.indices) {
            setFluenceAngle(newStep.stepToDegree(totalSteps = fluenceList.value.size))

            val pulseStep = pulseAngle.value.degreeToStep(totalSteps = PulseDurations.size)
            val currentPulseDuration = PulseDurations[pulseStep]
            val newFluence = fluenceList.value[newStep]

            // Call the helper without resetting the fluence angle.
            updateLaserParameters(
                newPulseDuration = currentPulseDuration,
                newFluence = newFluence
            )
        }
    }

    fun onChangeRepetition(angle: Float) {
        // This logic is simple and has no complex dependencies, so it can remain as is.
        if (angle != repetitionAngle.value) {
            setRepetitionAngle(angle)
        }
        setSelectedPresetIndex(0)
    }

    fun onClickRepetition(state: UpDownState) {
        // This logic is also simple and correct.
        val currentStep = repetitionAngle.value.degreeToStep(totalSteps = repetitionList.value.size)
        val newStep = if (state == UpDownState.Up) currentStep + 1 else currentStep - 1

        if (newStep in repetitionList.value.indices) {
            val newRepetitionAngle = newStep.stepToDegree(totalSteps = repetitionList.value.size)
            setRepetitionAngle(newRepetitionAngle)
            setSelectedPresetIndex(0)
        }
    }

    /**
     * Perform all heavy I/O in a single background block.
     * This prevents the "Skipped frames" caused by 30+ sequential bridge calls
     * from MainActivity to MainViewModel.
     */
    suspend fun performFullInitialization() {
        withContext(dispatcherProvider.io) {
            try {
                // 1. Database tasks (Heavy)
                trimSerialLog()
                loadTemperatureDataFromDatabase()

                // 2. Load all preferences in parallel to save time
                // This reduces the total time the background thread is "busy"
                val tasks = listOf(
                    launch { loadHpCountFromPreference() },
                    launch { loadLampCountFromPreference() },
                    launch { loadDcdCountFromPreference() },
                    launch { initSliderVolumeFromPreference() },
                    launch { loadVoltageTableFromPreference() },
                    launch { loadSprayDcdIndexFromPreference() },
                    launch { loadPresetListFromPreference() },
                    launch { loadSprayDcdFromPreference() },
                    launch { loadSprayDcdListFromPreference() },
                    launch { loadGuideBeamFromPreference() },
                    launch { loadGuideBeamMaxFromPreference() },
                    launch { loadGuideBeamMinFromPreference() },
                    launch { loadOpTimeHourFromPreference() },
                    launch { loadQSwitchFromPreference() },
                    launch { loadLifeTimeFromPreference() },
                    launch { loadTemeratureFromPreference() },
                    launch { loadTemeratureWriteFromPreference() },
                    launch { loadEnergyMeasuredWriteFromPreference() },
                    launch { loadEnergyRefer2FromPreference() },
                    launch { loadProductSerialListFromPreference() },
                    launch { loadPowerSupplySerialListFromPreference() },
                    launch { loadLaserHandSerialListFromPreference() }
                )
                tasks.joinAll() // Wait for all to finish

                Timber.d("Initialization complete. UI switched.")
            } catch (e: Exception) {
                Timber.e(e, "performFullInitialization failed")
            }
        }
    }

    init {
        Timber.d("MainViewModel instance created")
        // ONLY do light, non-IO variable initialization here.
        // DO NOT call load... functions here.
    }
}