import uuid
from typing import List

from opentrons.protocol_api import Well, TrashBin, WasteChute
from opentrons.types import Location

from ..data.data import Data

from ..data.dbo.instrument import InstrumentWrapper


class InstrumentLogic:
    def __init__(self, data: Data):
        self.data = data

    @staticmethod
    def instrument_is_valid(instrument: InstrumentWrapper | None):
        return instrument is not None and instrument.instrument_instance is not None

    def home_plunger(self, instrument_id: uuid.UUID):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        instrument.instrument_instance.home_plunger()

    def configure_for_volume(self, instrument_id: uuid.UUID, volume: float):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        instrument.instrument_instance.configure_for_volume(volume=volume)

    def set_tip_racks(self, instrument_id: uuid.UUID, tip_rack_ids: List[uuid.UUID]):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        tip_racks = []
        for tip_rack_id in tip_rack_ids:
            tip_rack = self.data.static_items.get_labware_by_id(tip_rack_id)
            if tip_rack is None or tip_rack.labware_instance is None or not tip_rack.labware_instance.is_tiprack:
                raise RuntimeError("Tip rack id not valid")
            tip_racks.append(tip_rack.labware_instance)

        instrument.data.tip_racks = tip_rack_ids
        instrument.instrument_instance.tip_racks = tip_racks

    def drop_tip(self, instrument_id: uuid.UUID, home_after: bool = False):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        instrument.instrument_instance.drop_tip(home_after=home_after)

    def drop_tip_location(self, instrument_id: uuid.UUID, location_id: uuid.UUID, home_after: bool = False):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        location = self.data.get_well_location_trash_by_id(location_id)

        if location is None or isinstance(location, Well):
            raise RuntimeError("Location or Trash id not valid")

        instrument.instrument_instance.drop_tip(home_after=home_after)

    def pick_up_tip(self, instrument_id: uuid.UUID):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        instrument.instrument_instance.pick_up_tip()

    def return_tip(self, instrument_id: uuid.UUID):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        instrument.instrument_instance.return_tip()

    @staticmethod
    def prepare_air_gap_parameters(volume, height):
        volume_value: float | None = volume
        height_value: float | None = height
        if volume_value == 0:
            volume_value = None
        if height_value == 0:
            height_value = None
        return height_value, volume_value

    def air_gap(self, instrument_id: uuid.UUID, volume: float = 0, height: float = 0):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        height_value, volume_value = self.prepare_air_gap_parameters(volume, height)

        instrument.instrument_instance.air_gap(volume=volume_value, height=height_value)

    @staticmethod
    def prepare_aspirate_parameters(rate, volume):
        volume_value: float | None = volume
        rate_value: float | None = rate
        if volume_value == 0:
            volume_value = None
        if rate_value == 0:
            rate_value = None
        return rate_value, volume_value

    def aspirate(self, instrument_id: uuid.UUID, volume: float = 0, rate: float = 0):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        rate_value, volume_value = self.prepare_aspirate_parameters(volume, rate)

        instrument.instrument_instance.aspirate(volume=volume_value, rate=rate_value)

    def aspirate_location(self, instrument_id: uuid.UUID, location_id: uuid.UUID, volume: float = 0, rate: float = 0):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        rate_value, volume_value = self.prepare_aspirate_parameters(volume, rate)

        location = self.data.dynamic_items.get_well_location_by_id(location_id)

        if location is None:
            raise RuntimeError("Well location id not valid")

        instrument.instrument_instance.aspirate(volume=volume_value, location=location, rate=rate_value)

    def blow_out(self, instrument_id: uuid.UUID):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        instrument.instrument_instance.blow_out()

    def blow_out_location(self, instrument_id: uuid.UUID, location_id: uuid.UUID):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        location = self.data.get_well_location_trash_by_id(location_id)

        if location is None:
            raise RuntimeError("Well, Location or Trash id not valid")

        instrument.instrument_instance.blow_out(location=location)

    @staticmethod
    def prepare_mix_variables(rate, repetitions, volume):
        repetitions_value: int = repetitions
        volume_value: float | None = volume
        rate_value: float | None = rate
        if repetitions_value == 0:
            repetitions_value = 1
        if volume_value == 0:
            volume_value = None
        if rate_value == 0:
            rate_value = None
        return rate_value, repetitions_value, volume_value

    def mix(self, instrument_id: uuid.UUID, location_id: uuid.UUID, repetitions: int = 0, volume: float = 0, rate: float = 0):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        rate_value, repetitions_value, volume_value = self.prepare_mix_variables(rate, repetitions, volume)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        instrument.instrument_instance.mix(repetitions=repetitions_value, volume=volume_value, rate=rate_value)

    def mix_location(self, instrument_id: uuid.UUID, location_id: uuid.UUID, repetitions: int = 0, volume: float = 0, rate: float = 0):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        rate_value, repetitions_value, volume_value = self.prepare_mix_variables(rate, repetitions, volume)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        location = self.data.dynamic_items.get_well_location_by_id(location_id)

        if location is None:
            raise RuntimeError("Well location id not valid")

        instrument.instrument_instance.mix(location=location, repetitions=repetitions_value, volume=volume_value, rate=rate_value)

    @staticmethod
    def prepare_dispense_parameters(push_out, rate, volume):
        volume_value: float | None = volume
        rate_value: float | None = rate
        push_out_value: float | None = push_out
        if volume_value == 0:
            volume_value = None
        if rate_value == 0:
            rate_value = None
        if push_out_value == 0:
            push_out_value = None
        return push_out_value, rate_value, volume_value

    def dispense(self, instrument_id: uuid.UUID, volume: float = 0, rate: float = 0, push_out: float = 0):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        push_out_value, rate_value, volume_value = self.prepare_dispense_parameters(push_out, rate, volume)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        instrument.instrument_instance.dispense(volume=volume_value, rate=rate_value, push_out=push_out_value)

    def dispense_location(self, instrument_id: uuid.UUID, location_id: uuid.UUID, volume: float = 0, rate: float = 0, push_out: float = 0):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        push_out_value, rate_value, volume_value = self.prepare_dispense_parameters(push_out, rate, volume)

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        location = self.data.get_well_location_trash_by_id(location_id)

        if location is None:
            raise RuntimeError("Well, Location or Trash id not valid")

        instrument.instrument_instance.dispense(volume=volume_value, location=location, rate=rate_value,
                                                push_out=push_out_value)

    def move_to_location(self, instrument_id: uuid.UUID, location_id: uuid.UUID, force_direct: bool = False, speed: float = 0):
        instrument = self.data.static_items.get_instrument_by_id(instrument_id)

        if speed == 0:
            speed_value = None
        else:
            speed_value = speed

        if not self.instrument_is_valid(instrument):
            raise RuntimeError("Instrument id not valid")

        location = self.data.get_well_location_trash_by_id(location_id)

        if location is None or isinstance(location, Well):
            raise RuntimeError("Location or Trash id not valid")

        instrument.instrument_instance.move_to(location=location, force_direct=force_direct, speed=speed_value)