import datetime

from marshmallow import post_load, pre_load, fields as f
from marshmallow.fields import Boolean, Date, DateTime, Float, Integer, List, Str, String, UUID, Dict
from marshmallow.validate import Length, OneOf, Range
from sqlalchemy.util import OrderedSet
from stdnum import imei, meid
from teal.enums import Layouts
from teal.marshmallow import EnumField, SanitizedStr, URL, ValidationError
from teal.resource import Schema

from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources import enums
from ereuse_devicehub.resources.device import models as m, states
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing, UnitCodes


class Device(Thing):
    __doc__ = m.Device.__doc__
    id = Integer(description=m.Device.id.comment, dump_only=True)
    hid = SanitizedStr(lower=True, description=m.Device.hid.comment)
    tags = NestedOn('Tag',
                    many=True,
                    collection_class=OrderedSet,
                    description='A set of tags that identify the device.')
    model = SanitizedStr(lower=True,
                         validate=Length(max=STR_BIG_SIZE),
                         description=m.Device.model.comment)
    manufacturer = SanitizedStr(lower=True,
                                validate=Length(max=STR_SIZE),
                                description=m.Device.manufacturer.comment)
    serial_number = SanitizedStr(lower=True,
                                 validate=Length(max=STR_BIG_SIZE),
                                 data_key='serialNumber')
    brand = SanitizedStr(validate=Length(max=STR_BIG_SIZE), description=m.Device.brand.comment)
    generation = Integer(validate=Range(1, 100), description=m.Device.generation.comment)
    version = SanitizedStr(description=m.Device.version)
    weight = Float(validate=Range(0.1, 5), unit=UnitCodes.kgm, description=m.Device.weight.comment)
    width = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.width.comment)
    height = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.height.comment)
    depth = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.depth.comment)
    # TODO TimeOut 2. Comment actions and lots if there are time out.
    actions = NestedOn('Action', many=True, dump_only=True, description=m.Device.actions.__doc__)
    # TODO TimeOut 2. Comment actions_one and lots if there are time out.
    actions_one = NestedOn('Action', many=True, load_only=True, collection_class=OrderedSet)
    problems = NestedOn('Action', many=True, dump_only=True, description=m.Device.problems.__doc__)
    url = URL(dump_only=True, description=m.Device.url.__doc__)
    # TODO TimeOut 2. Comment actions and lots if there are time out.
    lots = NestedOn('Lot',
                    many=True,
                    dump_only=True,
                    description='The lots where this device is directly under.')
    rate = NestedOn('Rate', dump_only=True, description=m.Device.rate.__doc__)
    price = NestedOn('Price', dump_only=True, description=m.Device.price.__doc__)
    tradings = Dict(dump_only=True, description='')
    physical = EnumField(states.Physical, dump_only=True, description=m.Device.physical.__doc__)
    traking = EnumField(states.Traking, dump_only=True, description=m.Device.physical.__doc__)
    usage = EnumField(states.Usage, dump_only=True, description=m.Device.physical.__doc__)
    revoke = UUID(dump_only=True)
    physical_possessor = NestedOn('Agent', dump_only=True, data_key='physicalPossessor')
    production_date = DateTime('iso',
                               description=m.Device.updated.comment,
                               data_key='productionDate')
    working = NestedOn('Action',
                       many=True,
                       dump_only=True,
                       description=m.Device.working.__doc__)
    variant = SanitizedStr(description=m.Device.variant.comment)
    sku = SanitizedStr(description=m.Device.sku.comment)
    image = URL(description=m.Device.image.comment)
    allocated = Boolean(description=m.Device.allocated.comment)
    devicehub_id = SanitizedStr(data_key='devicehubID',
                                description=m.Device.devicehub_id.comment)

    @pre_load
    def from_actions_to_actions_one(self, data: dict):
        """
        Not an elegant way of allowing submitting actions to a device
        (in the context of Snapshots) without creating an ``actions``
        field at the model (which is not possible).
        :param data:
        :return:
        """
        # Note that it is secure to allow uploading actions_one
        # as the only time an user can send a device object is
        # in snapshots.
        data['actions_one'] = data.pop('actions', [])
        return data

    @post_load
    def validate_snapshot_actions(self, data):
        """Validates that only snapshot-related actions can be uploaded."""
        from ereuse_devicehub.resources.action.models import EraseBasic, Test, Rate, Install, \
            Benchmark
        for action in data['actions_one']:
            if not isinstance(action, (Install, EraseBasic, Rate, Test, Benchmark)):
                raise ValidationError('You cannot upload {}'.format(action),
                                      field_names=['actions'])


class Computer(Device):
    __doc__ = m.Computer.__doc__
    # TODO TimeOut 1. Comment components if there are time out.
    components = NestedOn('Component',
                          many=True,
                          dump_only=True,
                          collection_class=OrderedSet,
                          description='The components that are inside this computer.')
    chassis = EnumField(enums.ComputerChassis,
                        description=m.Computer.chassis.comment)
    ram_size = Integer(dump_only=True,
                       data_key='ramSize',
                       description=m.Computer.ram_size.__doc__)
    data_storage_size = Integer(dump_only=True,
                                data_key='dataStorageSize',
                                description=m.Computer.data_storage_size.__doc__)
    processor_model = Str(dump_only=True,
                          data_key='processorModel',
                          description=m.Computer.processor_model.__doc__)
    graphic_card_model = Str(dump_only=True,
                             data_key='graphicCardModel',
                             description=m.Computer.graphic_card_model.__doc__)
    network_speeds = List(Integer(dump_only=True),
                          dump_only=True,
                          data_key='networkSpeeds',
                          description=m.Computer.network_speeds.__doc__)
    privacy = NestedOn('Action',
                       many=True,
                       dump_only=True,
                       collection_class=set,
                       description=m.Computer.privacy.__doc__)
    amount = Integer(validate=f.validate.Range(min=0, max=100),
                      description=m.Computer.amount.__doc__)
    # author_id = NestedOn(s_user.User, only_query='author_id')
    owner_id = UUID(data_key='ownerID')
    transfer_state = EnumField(enums.TransferState, description=m.Computer.transfer_state.comment)
    receiver_id = UUID(data_key='receiverID')
    uuid = UUID(required=False)


class Desktop(Computer):
    __doc__ = m.Desktop.__doc__


class Laptop(Computer):
    __doc__ = m.Laptop.__doc__
    layout = EnumField(Layouts, description=m.Laptop.layout.comment)


class Server(Computer):
    __doc__ = m.Server.__doc__


class DisplayMixin:
    __doc__ = m.DisplayMixin.__doc__
    size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150))
    technology = EnumField(enums.DisplayTech,
                           description=m.DisplayMixin.technology.comment)
    resolution_width = Integer(data_key='resolutionWidth',
                               validate=Range(10, 20000),
                               description=m.DisplayMixin.resolution_width.comment,
                               )
    resolution_height = Integer(data_key='resolutionHeight',
                                validate=Range(10, 20000),
                                description=m.DisplayMixin.resolution_height.comment,
                                )
    refresh_rate = Integer(data_key='refreshRate', validate=Range(10, 1000))
    contrast_ratio = Integer(data_key='contrastRatio', validate=Range(100, 100000))
    touchable = Boolean(description=m.DisplayMixin.touchable.comment)
    aspect_ratio = String(dump_only=True, description=m.DisplayMixin.aspect_ratio.__doc__)
    widescreen = Boolean(dump_only=True, description=m.DisplayMixin.widescreen.__doc__)


class NetworkMixin:
    __doc__ = m.NetworkMixin.__doc__

    speed = Integer(validate=Range(min=10, max=10000),
                    unit=UnitCodes.mbps,
                    description=m.NetworkAdapter.speed.comment)
    wireless = Boolean(required=True)


class Monitor(DisplayMixin, Device):
    __doc__ = m.Monitor.__doc__


class ComputerMonitor(Monitor):
    __doc__ = m.ComputerMonitor.__doc__


class TelevisionSet(Monitor):
    __doc__ = m.TelevisionSet.__doc__


class Mobile(Device):
    __doc__ = m.Mobile.__doc__

    imei = Integer(description=m.Mobile.imei.comment)
    meid = Str(description=m.Mobile.meid.comment)
    ram_size = Integer(validate=Range(min=128, max=36000),
                       data_key='ramSize',
                       unit=UnitCodes.mbyte,
                       description=m.Mobile.ram_size.comment)
    data_storage_size = Integer(validate=Range(0, 10 ** 8),
                                data_key='dataStorageSize',
                                description=m.Mobile.data_storage_size)
    display_size = Float(validate=Range(min=0.1, max=30.0),
                         data_key='displaySize',
                         description=m.Mobile.display_size.comment)

    @pre_load
    def convert_check_imei(self, data):
        if data.get('imei', None):
            data['imei'] = int(imei.validate(data['imei']))
        return data

    @pre_load
    def convert_check_meid(self, data: dict):
        if data.get('meid', None):
            data['meid'] = meid.compact(data['meid'])
        return data


class Smartphone(Mobile):
    __doc__ = m.Smartphone.__doc__


class Tablet(Mobile):
    __doc__ = m.Tablet.__doc__


class Cellphone(Mobile):
    __doc__ = m.Cellphone.__doc__


class Component(Device):
    __doc__ = m.Component.__doc__

    parent = NestedOn(Device, dump_only=True)


class GraphicCard(Component):
    __doc__ = m.GraphicCard.__doc__

    memory = Integer(validate=Range(0, 10000),
                     unit=UnitCodes.mbyte,
                     description=m.GraphicCard.memory.comment)


class DataStorage(Component):
    __doc__ = m.DataStorage.__doc__

    size = Integer(validate=Range(0, 10 ** 8),
                   unit=UnitCodes.mbyte,
                   description=m.DataStorage.size.comment)
    interface = EnumField(enums.DataStorageInterface)
    privacy = NestedOn('Action', dump_only=True)


class HardDrive(DataStorage):
    __doc__ = m.HardDrive.__doc__


class SolidStateDrive(DataStorage):
    __doc__ = m.SolidStateDrive.__doc__


class Motherboard(Component):
    __doc__ = m.Motherboard.__doc__

    slots = Integer(validate=Range(0, 20),
                    description=m.Motherboard.slots.comment)
    usb = Integer(validate=Range(0, 20), description=m.Motherboard.usb.comment)
    firewire = Integer(validate=Range(0, 20), description=m.Motherboard.firewire.comment)
    serial = Integer(validate=Range(0, 20), description=m.Motherboard.serial.comment)
    pcmcia = Integer(validate=Range(0, 20), description=m.Motherboard.pcmcia.comment)
    bios_date = Date(validate=Range(datetime.date(year=1980, month=1, day=1),
                                    datetime.date(year=2030, month=1, day=1)),
                     data_key='biosDate',
                     description=m.Motherboard.bios_date)
    ram_slots = Integer(validate=Range(1), data_key='ramSlots')
    ram_max_size = Integer(validate=Range(1), data_key='ramMaxSize')


class NetworkAdapter(NetworkMixin, Component):
    __doc__ = m.NetworkAdapter.__doc__


class Processor(Component):
    __doc__ = m.Processor.__doc__

    speed = Float(validate=Range(min=0.1, max=15),
                  unit=UnitCodes.ghz,
                  description=m.Processor.speed.comment)
    cores = Integer(validate=Range(min=1, max=10), description=m.Processor.cores.comment)
    threads = Integer(validate=Range(min=1, max=20), description=m.Processor.threads.comment)
    address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}),
                      description=m.Processor.address.comment)
    abi = SanitizedStr(lower=True, description=m.Processor.abi.comment)


class RamModule(Component):
    __doc__ = m.RamModule.__doc__

    size = Integer(validate=Range(min=128, max=17000),
                   unit=UnitCodes.mbyte,
                   description=m.RamModule.size.comment)
    speed = Integer(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
    interface = EnumField(enums.RamInterface)
    format = EnumField(enums.RamFormat)


class SoundCard(Component):
    __doc__ = m.SoundCard.__doc__


class Display(DisplayMixin, Component):
    __doc__ = m.Display.__doc__


class Battery(Component):
    __doc__ = m.Battery.__doc__

    wireless = Boolean(description=m.Battery.wireless.comment)
    technology = EnumField(enums.BatteryTechnology, description=m.Battery.technology.comment)
    size = Integer(required=True, description=m.Battery.size.comment)


class Camera(Component):
    __doc__ = m.Camera.__doc__

    focal_length = Integer(data_key='focalLength')
    video_height = Integer(data_key='videoHeight')
    video_width = Integer(data_key='videoWidth')
    horizontal_view_angle = Integer(data_key='horizontalViewAngle')
    facing = Integer()
    vertical_view_angle = Integer(data_key='verticalViewAngle')
    video_stabilization = Integer(data_key='videoStabilization')
    flash = Integer()


class Manufacturer(Schema):
    __doc__ = m.Manufacturer.__doc__

    name = String(dump_only=True)
    url = URL(dump_only=True)
    logo = URL(dump_only=True)


class ComputerAccessory(Device):
    __doc__ = m.ComputerAccessory.__doc__


class Mouse(ComputerAccessory):
    __doc__ = m.Mouse.__doc__


class MemoryCardReader(ComputerAccessory):
    __doc__ = m.MemoryCardReader.__doc__


class SAI(ComputerAccessory):
    __doc__ = m.SAI.__doc__


class Keyboard(ComputerAccessory):
    __doc__ = m.Keyboard.__doc__

    layout = EnumField(Layouts)


class Networking(NetworkMixin, Device):
    __doc__ = m.Networking.__doc__


class Router(Networking):
    __doc__ = m.Router.__doc__


class Switch(Networking):
    __doc__ = m.Switch.__doc__


class Hub(Networking):
    __doc__ = m.Hub.__doc__


class WirelessAccessPoint(Networking):
    __doc__ = m.WirelessAccessPoint.__doc__


class Printer(Device):
    __doc__ = m.Printer.__doc__

    wireless = Boolean(required=True, missing=False, description=m.Printer.wireless.comment)
    scanning = Boolean(required=True, missing=False, description=m.Printer.scanning.comment)
    technology = EnumField(enums.PrinterTechnology,
                           required=True,
                           description=m.Printer.technology.comment)
    monochrome = Boolean(required=True, missing=True, description=m.Printer.monochrome.comment)


class LabelPrinter(Printer):
    __doc__ = m.LabelPrinter.__doc__


class Sound(Device):
    __doc__ = m.Sound.__doc__


class Microphone(Sound):
    __doc__ = m.Microphone.__doc__


class Video(Device):
    __doc__ = m.Video.__doc__


class VideoScaler(Video):
    __doc__ = m.VideoScaler.__doc__


class Videoconference(Video):
    __doc__ = m.Videoconference.__doc__


class Cooking(Device):
    __doc__ = m.Cooking.__doc__


class Mixer(Cooking):
    __doc__ = m.Mixer.__doc__


class DIYAndGardening(Device):
    pass


class Drill(DIYAndGardening):
    max_drill_bit_size = Integer(data_key='maxDrillBitSize')


class PackOfScrewdrivers(DIYAndGardening):
    size = Integer()


class Home(Device):
    pass


class Dehumidifier(Home):
    size = Integer()


class Stairs(Home):
    max_allowed_weight = Integer(data_key='maxAllowedWeight')


class Recreation(Device):
    pass


class Bike(Recreation):
    wheel_size = Integer(data_key='wheelSize')
    gears = Integer()


class Racket(Recreation):
    pass