import asyncio
import logging
from typing import Mapping, Optional
from opentrons.drivers.mag_deck import (
    SimulatingDriver, MagDeckDriver, AbstractMagDeckDriver)
from opentrons.drivers.rpi_drivers.types import USBPort
from ..execution_manager import ExecutionManager
from . import update, mod_abc, types

log = logging.getLogger(__name__)

MAX_ENGAGE_HEIGHT = {
    # mm from home position
    'magneticModuleV1': 45,
    'magneticModuleV2': 25
}
OFFSET_TO_LABWARE_BOTTOM = {
    'magneticModuleV1': 5,
    'magneticModuleV2': 2.5
}


class MagDeck(mod_abc.AbstractModule):

    FIRST_GEN2_REVISION = 20

    @classmethod
    async def build(cls,
                    port: str,
                    usb_port: USBPort,
                    execution_manager: ExecutionManager,
                    interrupt_callback: types.InterruptCallback = None,
                    simulating=False,
                    loop: asyncio.AbstractEventLoop = None,
                    sim_model: str = None,
                    **kwargs):
        """Factory function."""
        driver: AbstractMagDeckDriver
        if not simulating:
            driver = await MagDeckDriver.create(port=port, loop=loop)
        else:
            driver = SimulatingDriver(sim_model=sim_model)

        mod = cls(port=port,
                  usb_port=usb_port,
                  loop=loop,
                  execution_manager=execution_manager,
                  device_info=await driver.get_device_info(),
                  driver=driver)
        return mod

    def __init__(self,
                 port: str,
                 usb_port: USBPort,
                 execution_manager: ExecutionManager,
                 driver: AbstractMagDeckDriver,
                 device_info: Mapping[str, str],
                 loop: asyncio.AbstractEventLoop = None) -> None:
        """Constructor"""
        super().__init__(port=port,
                         usb_port=usb_port,
                         loop=loop,
                         execution_manager=execution_manager)
        self._device_info = device_info
        self._driver = driver
        self._current_height = 0.0

    @classmethod
    def name(cls) -> str:
        """Get the module name."""
        return 'magdeck'

    def model(self) -> str:
        """Get the model."""
        return self._model_from_revision(self._device_info.get('model'))

    @classmethod
    def bootloader(cls) -> types.UploadFunction:
        """Get the bootloating method."""
        return update.upload_via_avrdude

    async def calibrate(self):
        """Calibration involves probing for top plate to get the plate height."""
        await self.wait_for_is_running()
        await self._driver.probe_plate()
        # return if successful or not?

    async def engage(self, height: float):
        """Move the magnet to a specific height, in mm from home position."""
        await self.wait_for_is_running()
        if height > MAX_ENGAGE_HEIGHT[self.model()] or height < 0:
            raise ValueError(
                f'Invalid engage height for {self.model()}: {height} mm. '
                f'Must be 0 - {MAX_ENGAGE_HEIGHT[self.model()]} mm')
        await self._driver.move(height)
        self._current_height = await self._driver.get_mag_position()

    async def deactivate(self):
        """Home the magnet."""
        await self.wait_for_is_running()
        await self._driver.home()
        await self.engage(0.0)

    @property
    def current_height(self) -> float:
        """Get the current height."""
        return self._current_height

    @property
    def device_info(self) -> Mapping[str, str]:
        """

        Returns: a dict
            { 'serial': 'abc123', 'model': '8675309', 'version': '9001' }

        """
        return self._device_info

    @property
    def status(self) -> str:
        if self.current_height > 0:
            return 'engaged'
        else:
            return 'disengaged'

    @property
    def engaged(self) -> bool:
        if self.current_height > 0:
            return True
        else:
            return False

    @property
    def live_data(self) -> types.LiveData:
        return {
            'status': self.status,
            'data': {
                'engaged': self.engaged,
                'height': self.current_height
            }
        }

    @property
    def is_simulated(self) -> bool:
        return isinstance(self._driver, SimulatingDriver)

    # Internal Methods

    async def prep_for_update(self) -> str:
        await self._driver.enter_programming_mode()
        new_port = await update.find_bootloader_port()
        return new_port or self.port

    @staticmethod
    def _model_from_revision(revision: Optional[str]) -> str:
        """ Defines the revision -> model mapping """
        if not revision or 'v' not in revision:
            log.error(f'bad revision: {revision}')
            return 'magneticModuleV1'
        try:
            revision_num = float(revision.split('v')[-1])  # type: ignore
        except (ValueError, TypeError):
            log.exception('bad revision: {revision}')
            return 'magneticModuleV1'
        if revision_num < MagDeck.FIRST_GEN2_REVISION:
            return 'magneticModuleV1'
        else:
            return 'magneticModuleV2'
