from ..PickList import PickList
from ..Well import TransferError
from ..tools import round_at, did_you_mean


class AssemblyPicklistGenerator:
    """Class to generate robot picklists to mix genetic parts for DNA assembly.

    (for TECAN EVO, Labcyte ECHO, etc.)
    
    Parameters
    ----------

    part_mol, part_l, part_g
      Amount of each DNA part that an assembly mix should contain (provide
      either one of these). Parameter ``part_mol`` indicates a amount of
      molecules in mol, e.g. 13e-12 to specify "13 pico-mol of each DNA part". 

    complement_to

    buffer_volume

    volume_rounding

    minimal_dispense_volume



    """

    def __init__(
        self,
        part_mol=None,
        part_l=None,
        part_g=None,
        complement_to=None,
        buffer_volume=0,
        volume_rounding=None,
        minimal_dispense_volume=0,
    ):

        self.part_mol = part_mol
        self.part_l = part_l
        self.part_g = part_g
        self.buffer_volume = buffer_volume
        self.complement_to = complement_to
        self.volume_rounding = volume_rounding
        self.minimal_dispense_volume = minimal_dispense_volume

    def make_picklist(
        self,
        assembly_plan,
        source_wells,
        destination_wells,
        buffer_well=None,
        complement_well=None,
    ):
        source_wells = list(source_wells)
        destination_wells = list(destination_wells)
        destination_wells = destination_wells[: len(assembly_plan.assemblies)]

        part_wells = {}
        duplicates = {}
        for well in source_wells:
            well_part = self.get_part_from_well(well)
            if well_part in part_wells:
                if well_part not in duplicates:
                    duplicates[well_part] = []
                duplicates[well_part].append(well.name)
                previous_well = part_wells[well_part]
                previous_concentration = previous_well.content.concentration()
                if well.content.concentration() > previous_concentration:
                    part_wells[well_part] = well
            else:
                part_wells[well_part] = well

        required_parts = set(assembly_plan.all_parts_used())
        missing_parts = required_parts.difference(set(part_wells))
        if missing_parts:
            return (
                None,
                {
                    "missing_parts": {
                        part: {
                            "featured_in": assembly_plan.assemblies_featuring(
                                part
                            ),
                            "did_you_mean": [
                                (name, str(part_wells[name]))
                                for name in did_you_mean(
                                    part, part_wells, min_score=50
                                )
                            ],
                        }
                        for part in missing_parts
                    }
                },
            )

        picklist = PickList()
        iterator = zip(assembly_plan.assemblies.items(), destination_wells)
        for ((construct_name, parts), destination_well) in iterator:
            destination_well.data.construct = construct_name
            for part in parts:
                source_well = part_wells[part]
                volume = self.volume_from_well(
                    source_well, assembly_plan.parts_data
                )
                volume = round_at(volume, self.volume_rounding)
                volume = max(volume, self.minimal_dispense_volume)
                picklist.add_transfer(
                    source_well, destination_well, volume=volume
                )

        wells_over_desired_volume = []
        if self.complement_to is not None:
            if complement_well is None:
                for label in "COMPLEMENT", "WATER":
                    complement_well = part_wells.get(label, None)
                if complement_well is None:
                    raise ValueError(
                        "Could not identify the complement well ."
                        " in the plate. One well should be marked"
                        " as containing WATER or COMPLEMENT."
                    )
            if complement_well.is_empty:
                raise TransferError(
                    "Well with COMPLEMENT/WATER is empty: %s."
                    % (complement_well)
                )
            for well in destination_wells:
                to_well = picklist.restricted_to(destination_well=well)
                total_transfer_volume = to_well.total_transfered_volume()
                complement_volume = (
                    self.complement_to
                    - total_transfer_volume
                    - self.buffer_volume
                )
                if complement_volume < 0:
                    wells_over_desired_volume.append(well)
                else:
                    picklist.add_transfer(
                        source_well=complement_well,
                        destination_well=well,
                        volume=complement_volume,
                    )
        if self.buffer_volume:
            if buffer_well is None:
                buffer_well = part_wells.get("BUFFER", None)
            if buffer_well.is_empty:
                raise TransferError("Empty buffer well: %s." % (buffer_well))
            buffer_volume = round_at(self.buffer_volume, self.volume_rounding)
            for well in destination_wells:
                picklist.add_transfer(buffer_well, well, volume=buffer_volume)

        return (
            picklist,
            {
                "wells_over_desired_volume": wells_over_desired_volume,
                "duplicates": {
                    part_name: {
                        "wells": wells,
                        "selected": part_wells[part_name],
                    }
                    for part_name, wells in duplicates.items()
                },
            },
        )

    @staticmethod
    def get_part_from_well(well):
        if "part" in well.data:
            return well.data["part"]
        else:
            return well.content.components_as_string().strip(" ")

    def get_part_molar_weight(self, part_data):
        """Returns the molar weight of the sequence in g/m.
        http://cels.uri.edu/gsc/cndna.html
        """
        if "size" in part_data:
            size = part_data["size"]
        else:
            size = len(part_data["record"])
        return 650 * size

    def volume_from_well(self, well, parts_data):
        if well.content.volume == 0:
            raise ValueError(
                ("Cannot get anything from well %s " % well)
                + "which has a volume of 0"
            )
        part_mol = self.part_mol
        part_g = self.part_g
        part_l = self.part_l
        if part_mol is not None:
            part = self.get_part_from_well(well)
            molar_weight = self.get_part_molar_weight(parts_data[part])
            part_g = molar_weight * part_mol
        if part_g is not None:
            part_l = part_g / well.content.concentration()
        return part_l
