"""Insteon All-Link Database.

The All-Link database contains database records that represent links to other
Insteon devices that either respond to or control the current device.
"""
import asyncio
import logging

from ..constants import ALDBStatus, EngineVersion, ReadWriteMode
from ..managers.aldb_read_manager import ALDBReadManager
from .aldb_base import ALDBBase

_LOGGER = logging.getLogger(__name__)


class ALDB(ALDBBase):
    """All-Link Database for a device."""

    def __init__(
        self,
        address,
        version=EngineVersion.UNKNOWN,
        mem_addr=0x0FFF,
    ):
        """Init the ALDB class."""
        super().__init__(address=address, version=version, mem_addr=mem_addr)
        self._read_manager = ALDBReadManager(self._address, self._mem_addr)

    # pylint: disable=arguments-differ
    async def async_load(
        self, mem_addr: int = 0x00, num_recs: int = 0x00, refresh: bool = False
    ):
        """Load the All-Link Database."""
        _LOGGER.debug("Loading the ALDB async")
        self._update_status(ALDBStatus.LOADING)
        if refresh:
            self.clear()
        else:
            # Pop any unused records to make sure we query them
            unused = list(self.find(in_use=False))
            for rec in unused:
                self._records.pop(rec.mem_addr)

        if self._read_write_mode == ReadWriteMode.UNKNOWN:
            mode = ReadWriteMode.STANDARD
        else:
            mode = self._read_write_mode

        if self._mem_addr not in self._records:
            # Query the first record
            try:
                async for rec in self._read_manager.async_read(
                    mem_addr=0, num_recs=1, read_write_mode=mode
                ):
                    self._mem_addr = rec.mem_addr
                    self._add_record(rec)
            finally:
                await self._read_manager.async_stop()

        try:
            async for rec in self._read_manager.async_read(
                mem_addr=mem_addr,
                num_recs=num_recs,
                read_write_mode=mode,
            ):
                self._add_record(rec)
                await asyncio.sleep(0.1)
                if self._read_write_mode == ReadWriteMode.UNKNOWN:
                    self._read_write_mode = mode
                if self._is_loaded():
                    break
        finally:
            await self._read_manager.async_stop()

        if not self._is_loaded() and num_recs != 0:
            # Loading all records did not work so now we read individual missing records
            next_record = self._calc_next_record()
            while next_record:
                async for rec in self._read_manager.async_read(
                    mem_addr=next_record, num_recs=1
                ):
                    self._add_record(rec)
                prev_record = next_record
                next_record = self._calc_next_record()
                if next_record == prev_record:
                    # The ALDB did not return the requested record so stop
                    break

        if (
            not self._records
            and self._read_write_mode == ReadWriteMode.STANDARD
            and self._version not in [EngineVersion.I2CS, EngineVersion.OTHER]
        ):
            self._read_write_mode = ReadWriteMode.PEEK_POKE
            return await self.async_load(
                mem_addr=mem_addr, num_recs=num_recs, refresh=refresh
            )

        self.set_load_status()

        return self._status

    def _add_record(self, record) -> bool:
        """Add a record to the record set."""
        _LOGGER.debug("Loading record: %s", str(record))
        # Make sure the records make sense
        if (
            self.high_water_mark_mem_addr
            and record.mem_addr < self.high_water_mark_mem_addr
        ):
            _LOGGER.debug("Record is after the HWM: %s", str(record))
            return False

        # If an existing record will be replaced notify of change
        old_record = self._records.get(record.mem_addr)

        # If the old rec is identical to the new rec, do nothing
        if old_record and record.is_exact_match(old_record):
            _LOGGER.debug("Record has not changed:")
            _LOGGER.debug("Old: %s", str(old_record))
            _LOGGER.debug("New: %s", str(record))
            return False

        if old_record and old_record.is_in_use:
            self._notify_change(self._records[record.mem_addr], force_delete=True)

        self._records[record.mem_addr] = record
        self._notify_change(record)
        return True

    def _calc_next_record(self) -> int:
        """Calculate the memory address of the next missing record."""
        if not self._records:
            return self._mem_addr
        last_addr = list(self)[-1]
        if last_addr == self._mem_addr:
            return last_addr - 8

        for mem_addr in range(self._mem_addr, last_addr, -8):
            try:
                rec = self._records[mem_addr]
                if mem_addr == last_addr and rec.is_high_water_mark:
                    return None
            except IndexError:
                return mem_addr

        return last_addr - 8
