from __future__ import annotations
from dataclasses import dataclass
import logging
import json
import numbers
from typing import (Any, Dict, List, Union, Tuple,
                    Sequence, TYPE_CHECKING)

from opentrons import config
from opentrons.config import feature_flags as ff
from opentrons_shared_data.pipette import (
    model_config, name_config, fuse_specs)

if TYPE_CHECKING:
    from opentrons_shared_data.pipette.dev_types import (
        PipetteName, PipetteModel, UlPerMm, Quirk, PipetteFusedSpec
    )


log = logging.getLogger(__name__)


@dataclass
class PipetteConfig:
    top: float
    bottom: float
    blow_out: float
    drop_tip: float
    pick_up_current: float
    pick_up_distance: float
    pick_up_increment: float
    pick_up_presses: int
    pick_up_speed: float
    aspirate_flow_rate: float
    dispense_flow_rate: float
    channels: float
    model_offset: Tuple[float, float, float]
    plunger_current: float
    drop_tip_current: float
    drop_tip_speed: float
    min_volume: float
    max_volume: float
    ul_per_mm: UlPerMm
    quirks: List[Quirk]
    tip_length: float  # TODO(seth): remove
    # TODO: Replace entirely with tip length calibration
    tip_overlap: Dict[str, float]
    display_name: str
    name: PipetteName
    back_compat_names: List[PipetteName]
    return_tip_height: float
    blow_out_flow_rate: float
    max_travel: float
    home_position: float
    steps_per_mm: float
    idle_current: float
    default_blow_out_flow_rates: Dict[str, float]
    default_aspirate_flow_rates: Dict[str, float]
    default_dispense_flow_rates: Dict[str, float]
    model: PipetteModel


# Notes:
# - multi-channel pipettes share the same dimensional offsets
# - single-channel pipettes have different lengths
# - Default number of seconds to aspirate/dispense a pipette's full volume,
#     and these times were chosen to mimic normal human-pipetting motions.
#     However, accurate speeds are dependent on environment (ex: liquid
#     viscosity), therefore a pipette's flow-rates (ul/sec) should be set by
#     protocol writer

# Multi-channel y offset caluclations:
DISTANCE_BETWEEN_NOZZLES = 9
NUM_MULTI_CHANNEL_NOZZLES = 8
MULTI_LENGTH = (NUM_MULTI_CHANNEL_NOZZLES - 1) * DISTANCE_BETWEEN_NOZZLES
Y_OFFSET_MULTI = MULTI_LENGTH / 2
Z_OFFSET_MULTI = -25.8

Z_OFFSET_P10 = -13  # longest single-channel pipette
Z_OFFSET_P50 = 0
Z_OFFSET_P300 = 0
Z_OFFSET_P1000 = 20  # shortest single-channel pipette

LOW_CURRENT_DEFAULT = 0.05

config_models = list(model_config()['config'].keys())
config_names = list(name_config().keys())
configs = model_config()['config']
#: A list of pipette model names for which we have config entries
MUTABLE_CONFIGS = model_config()['mutableConfigs']
#: A list of mutable configs for pipettes
VALID_QUIRKS = model_config()['validQuirks']
#: A list of valid quirks for pipettes


def load(
        pipette_model: PipetteModel,
        pipette_id: str = None) -> PipetteConfig:
    """
    Load pipette config data


    This function loads from a combination of

    - the pipetteModelSpecs.json file in the wheel (should never be edited)
    - the pipetteNameSpecs.json file in the wheel (should never be edited)
    - any config overrides found in
      ``opentrons.config.CONFIG['pipette_config_overrides_dir']``

    This function reads from disk each time, so changes to the overrides
    will be picked up in subsequent calls.

    :param str pipette_model: The pipette model name (i.e. "p10_single_v1.3")
                              for which to load configuration
    :param pipette_id: An (optional) unique ID for the pipette to locate
                       config overrides. If the ID is not specified, the system
                       assumes this is a simulated pipette and does not
                       save settings. If the ID is specified but no overrides
                       corresponding to the ID are found, the system creates a
                       new overrides file for it.
    :type pipette_id: str or None
    :raises KeyError: if ``pipette_model`` is not in the top-level keys of
                      the pipetteModelSpecs.json file (and therefore not in
                      :py:attr:`configs`)

    :returns PipetteConfig: The configuration, loaded and checked
    """

    # Load the model config and update with the name config
    cfg = fuse_specs(pipette_model)

    # Load overrides if we have a pipette id
    if pipette_id:
        try:
            override = load_overrides(pipette_id)
            if 'quirks' in override.keys():
                override['quirks'] = [
                    qname for qname, qval in override['quirks'].items()
                    if qval]
            for legacy_key in (
                    'defaultAspirateFlowRate',
                    'defaultDispenseFlowRate',
                    'defaultBlowOutFlowRate'):
                override.pop(legacy_key, None)

        except FileNotFoundError:
            save_overrides(pipette_id, {}, pipette_model)
            log.info(
                "Save defaults for pipette model {} and id {}".format(
                    pipette_model, pipette_id))
        else:
            cfg.update(override)  # type: ignore

    # the ulPerMm functions are structured in pipetteModelSpecs.json as
    # a list sorted from oldest to newest. That means the latest functions
    # are always the last element and, as of right now, the older ones are
    # the first element (for models that only have one function, the first
    # and last elements are the same, which is fine). If we add more in the
    # future, we’ll have to change this code to select items more
    # intelligently
    if ff.use_old_aspiration_functions():
        log.debug("Using old aspiration functions")
        ul_per_mm = cfg['ulPerMm'][0]
    else:
        ul_per_mm = cfg['ulPerMm'][-1]

    smoothie_configs = cfg['smoothieConfigs']
    res = PipetteConfig(
        top=ensure_value(
            cfg, 'top', MUTABLE_CONFIGS),
        bottom=ensure_value(
            cfg, 'bottom', MUTABLE_CONFIGS),
        blow_out=ensure_value(
            cfg, 'blowout', MUTABLE_CONFIGS),
        drop_tip=ensure_value(
            cfg, 'dropTip', MUTABLE_CONFIGS),
        pick_up_current=ensure_value(cfg, 'pickUpCurrent', MUTABLE_CONFIGS),
        pick_up_distance=ensure_value(cfg, 'pickUpDistance', MUTABLE_CONFIGS),
        pick_up_increment=ensure_value(
            cfg, 'pickUpIncrement', MUTABLE_CONFIGS),
        pick_up_presses=ensure_value(cfg, 'pickUpPresses', MUTABLE_CONFIGS),
        pick_up_speed=ensure_value(cfg, 'pickUpSpeed', MUTABLE_CONFIGS),
        aspirate_flow_rate=cfg['defaultAspirateFlowRate']['value'],
        dispense_flow_rate=cfg['defaultDispenseFlowRate']['value'],
        channels=ensure_value(cfg, 'channels', MUTABLE_CONFIGS),
        model_offset=ensure_value(cfg, 'modelOffset', MUTABLE_CONFIGS),
        plunger_current=ensure_value(cfg, 'plungerCurrent', MUTABLE_CONFIGS),
        drop_tip_current=ensure_value(cfg, 'dropTipCurrent', MUTABLE_CONFIGS),
        drop_tip_speed=ensure_value(cfg, 'dropTipSpeed', MUTABLE_CONFIGS),
        min_volume=ensure_value(cfg, 'minVolume', MUTABLE_CONFIGS),
        max_volume=ensure_value(cfg, 'maxVolume', MUTABLE_CONFIGS),
        ul_per_mm=ul_per_mm,
        quirks=validate_quirks(ensure_value(cfg, 'quirks', MUTABLE_CONFIGS)),
        tip_overl
ap=cfg['tipOverlap'],
        tip_length=ensure_value(cfg, 'tipLength', MUTABLE_CONFIGS),
        display_name=ensure_value(cfg, 'displayName', MUTABLE_CONFIGS),
        name=cfg['name'],
        back_compat_names=cfg.get('backCompatNames', []),
        return_tip_height=cfg.get('returnTipHeight', 0.
5),
        blow_out_flow_rate=cfg['defaultBlowOutFlowRate']['value'],
        max_travel=smoothie_configs['travelDistance'],
        home_position=smoothie_configs['homePosition'],
        steps_per_mm=smoothie_configs['stepsPerMM'],
        idle_current=cfg.get('idleCurrent', LOW_CURRENT_DEFAULT),
        default_blow_out_flow_rates=cfg['defaultBlowOutFlowRate'].get(
            'valuesByApiLevel',
            {'2.0': cfg['defaultBlowOutFlowRate']['value']}),
        defaul