import socket

from .constants import (Command, CommandFlag, MonoEffect)
from .utils import clamp
from .WifiLedShopLightState import WifiLedShopLightState

class WifiLedShopLight:
  """
  A Wifi LED Shop Light
  """

  def __init__(self, ip, port = 8189, timeout = 5, retries = 5):
    """
    Creates a new Wifi LED Shop light

    :param ip: The IP of the controller on the network (STA Mode, not AP mode).
    :param port: The port the controller should listen on. It should almost always be left as the default.
    :param timeout: The timeout in seconds to wait listening to the socket.
    :param retries: The number of times to retry sending a command if it fails or times out before giving up.
    """
    self.ip = ip
    self.port = port
    self.timeout = timeout
    self.retries = retries
    self.state = WifiLedShopLightState()

    self.sock = None
    self.reconnect()

  def __enter__(self):
    return self

  def __exit__(self, type, value, traceback):
    self.close()

  def reconnect(self):
    """
    Try to (re-)connect to the controller via a socket
    """
    if self.sock:
      self.close()

    self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.sock.settimeout(self.timeout)
    self.sock.connect((self.ip, self.port))

  def close(self):
    """
    Closes the socket connection to the light
    """
    self.sock.close()
    self.sock = None

  def set_color(self, r=0, g=0, b=0):
    """
    Sets the color of the light (rgb each 0 to 255)
    """
    r = clamp(r)
    g = clamp(g)
    b = clamp(b)
    self.state.color = (r, g, b)
    self.send_command(Command.SET_COLOR, [int(r), int(g), int(b)])

  def set_brightness(self, brightness=0):
    """
    Sets the brightness of the light

    :param brightness: An int describing the brightness (0 to 255, where 255 is the brightest)
    """
    brightness = clamp(brightness)
    self.state.brightness = brightness
    self.send_command(Command.SET_BRIGHTNESS, [int(brightness)])

  def set_speed(self, speed=0):
    """
    Sets the speed of the effect. Not all effects use the speed, but it can be safely set regardless
    
    :param speed: An int describing the speed an effect will play at. (0 to 255, where 255 is the fastest)
    """
    speed = clamp(speed)
    self.state.speed = speed
    self.send_command(Command.SET_SPEED, [int(speed)])

  def set_preset(self, preset=0):
    """
    Sets the light effect to the provided built-in effect number

    :param preset: The preset effect to use. Valid values are 0 to 255. See the MonoEffect enum, or MONO_EFFECTS and PRESET_EFFECTS for mapping.
    """
    preset = clamp(preset)
    self.state.mode = preset
    self.send_command(Command.SET_PRESET, [int(preset)])

  def set_custom(self, custom):
    """
    Sets the light effect to the provided custom effect number
    
    :param custom: The custom effect to use. Valid values are 1 to 12. See the CustomEffect enum.
    """
    custom = clamp(custom, 1, 12)
    self.state.mode = custom
    self.send_command(Command.SET_CUSTOM, [int(custom)])

  def toggle(self):
    """
    Toggles the state of the light without checking the current state
    """
    self.state.is_on = not self.state.is_on
    self.send_command(Command.TOGGLE)

  def turn_on(self):
    """
    Toggles the light on only if it is not already on
    """
    if not self.state.is_on:
      self.toggle()

  def turn_off(self):
    """
    Toggles the light off only if it is not already off
    """
    if self.state.is_on:
      self.toggle()

  def set_segments(self, segments):
    """
    Sets the total number of segments. Total lights is segments * lights_per_segment.
    
    :param segments: The number of segments
    """
    self.send_command(Command.SET_SEGMENT_COUNT, [segments])

  def set_lights_per_segment(self, lights_per_segment):
    """
    Sets the number of lights per segment. Total lights is segments * lights_per_segment.

    :param lights_per_segment: The number of lights per segment
    """
    lights_per_segment_data = list(lights_per_segment.to_bytes(2, byteorder='little'))
    self.send_command(Command.SET_LIGHTS_PER_SEGMENT, lights_per_segment_data)

  def set_calculated_segments(self, total_lights, segments):
    """
    Helper function to automatically set the number of segments and lights per segment
    to reach the target total lights (rounded down to never exceed total_lights)

    Usually you know the total number of lights you have available on a light strip
    and want to split it into segments that take up the whole strip

    :param total_lights: The target total number of lights to use
    :param segments: The number of segments to split the total into
    """
    self.set_segments(segments)
    self.set_lights_per_segment(int(total_lights / segments))

  def send_command(self, command, data=[]):
    """
    Helper method to send a command to the controller.

    Mostly for internal use, prefer the specific functions where possible.
    
    Formats the low level message details like Start/End flag, binary data, and command

    :param command: The command to send to the controller. See the Command enum for valid commands.
    """
    min_data_len = 3
    padded_data = data + [0] * (min_data_len - len(data))
    raw_data = [CommandFlag.START, *padded_data, command, CommandFlag.END]
    self.send_bytes(raw_data)

  def send_bytes(self, data):
    """
    Helper method to send raw bytes directly to the controller

    Mostly for internal use, prefer the specific functions where possible
    """
    raw_data = bytes(data)

    attempts = 0
    while True:
      try:
        self.sock.sendall(raw_data)
        return
      except (socket.timeout, BrokenPipeError):
        if (attempts < self.retries):
          self.reconnect()
          attempts += 1
        else:
          raise

  def sync_state(self):
    """
    Syncs the state of the controller with the state of this object
    """
    attempts = 0
    while True:
      try:
        # Send the request for sync data
        self.send_command(Command.SYNC)

        response = self.sock.recv(1024)

        # Extract the state data
        state = bytearray(response)
        self.state.update_from_sync(state)
        return
      except (socket.timeout, BrokenPipeError):
        # When there is an error with the socket, close the connection and connect again
        if attempts < self.retries:
          self.reconnect()
          attempts += 1
        else:
          raise

  def __repr__(self):
    return f"""WikiLedShopLight @ {self.ip}:{self.port}
      state: {self.state}
    """

import socket

from .constants import (Command, CommandFlag, MonoEffect)
from .utils import clamp
from .WifiLedShopLightState import WifiLedShopLightState

class WifiLedShopLight:
  """
  A Wifi LED Shop Light
  """

  def __init__(self, ip, port = 8189, timeout = 5, retries = 5):
    """
    Creates a new Wifi LED Shop light

    :param ip: The IP of the controller on the network (STA Mode, not AP mode).
    :param port: The port the controller should listen on. It should almost always be left as the default.
    :param timeout: The timeout in seconds to wait listening to the socket.
    :param retries: The number of times to retry sending a command if it fails or times out before giving up.
    """
    self.ip = ip
    self.port = port
    self.timeout = timeout
    self.retries = retries
    self.state = WifiLedShopLightState()

    self.sock = None
    self.reconnect()

  def __enter__(self):
    return self

  def __exit__(self, type, value, traceback):
    self.close()

  def reconnect(self):
    """
    Try to (re-)connect to the controller via a socket
    """
    if self.sock:
      self.close()

    self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.sock.settimeout(self.timeout)
    self.sock.connect((self.ip, self.port))

  def close(self):
    """
    Closes the socket connection to the light
    """
    self.sock.close()
    self.sock = None

  def set_color(self, r=0, g=0, b=0):
    """
    Sets the color of the light (rgb each 0 to 255)
    """
    r = clamp(r)
    g = clamp(g)
    b = clamp(b)
    self.state.color = (r, g, b)
    self.send_command(Command.SET_COLOR, [int(r), int(g), int(b)])

  def set_brightness(self, brightness=0):
    """
    Sets the brightness of the light

    :param brightness: An int describing the brightness (0 to 255, where 255 is the brightest)
    """
    brightness = clamp(brightness)
    self.state.brightness = brightness
    self.send_command(Command.SET_BRIGHTNESS, [int(brightness)])

  def set_speed(self, speed=0):
    """
    Sets the speed of the effect. Not all effects use the speed, but it can be safely set regardless
    
    :param speed: An int describing the speed an effect will play at. (0 to 255, where 255 is the fastest)
    """
    speed = clamp(speed)
    self.state.speed = speed
    self.send_command(Command.SET_SPEED, [int(speed)])

  def set_preset(self, preset=0):
    """
    Sets the light effect to the provided built-in effect number

    :param preset: The preset effect to use. Valid values are 0 to 255. See the MonoEffect enum, or MONO_EFFECTS and PRESET_EFFECTS for mapping.
    """
    preset = clamp(preset)
    self.state.mode = preset
    self.send_command(Command.SET_PRESET, [int(preset)])

  def set_custom(self, custom):
    """
    Sets the light effect to the provided custom effect number
    
    :param custom: The custom effect to use. Valid values are 1 to 12. See the CustomEffect enum.
    """
    custom = clamp(custom, 1, 12)
    self.state.mode = custom
    self.send_command(Command.SET_CUSTOM, [int(custom)])

  def toggle(self):
    """
    Toggles the state of the light without checking the current state
    """
    self.state.is_on = not self.state.is_on
    self.send_command(Command.TOGGLE)

  def turn_on(self):
    """
    Toggles the light on only if it is not already on
    """
    if not self.state.is_on:
      self.toggle()

  def turn_off(self):
    """
    Toggles the light off only if it is not already off
    """
    if self.state.is_on:
      self.toggle()

  def set_segments(self, segments):
    """
    Sets the total number of segments. Total lights is segments * lights_per_segment.
    
    :param segments: The number of segments
    """
    self.send_command(Command.SET_SEGMENT_COUNT, [segments])

  def set_lights_per_segment(self, lights_per_segment):
    """
    Sets the number of lights per segment. Total lights is segments * lights_per_segment.

    :param lights_per_segment: The number of lights per segment
    """
    lights_per_segment_data = list(lights_per_segment.to_bytes(2, byteorder='little'))
    self.send_command(Command.SET_LIGHTS_PER_SEGMENT, lights_per_segment_data)

  def set_calculated_segments(self, total_lights, segments):
    """
    Helper function to automatically set the number of segments and lights per segment
    to reach the target total lights (rounded down to never exceed total_lights)

    Usually you know the total number of lights you have available on a light strip
    and want to split it into segments that take up the whole strip

    :param total_lights: The target total number of lights to use
    :param segments: The number of segments to split the total into
    """
    self.set_segments(segments)
    self.set_lights_per_segment(int(total_lights / segments))

  def send_command(self, command, data=[]):
    """
    Helper method to send a command to the controller.

    Mostly for internal use, prefer the specific functions where possible.
    
    Formats the low level message details like Start/End flag, binary data, and command

    :param command: The command to send to the controller. See the Command enum for valid commands.
    """
    min_data_len = 3
    padded_data = data + [0] * (min_data_len - len(data))
    raw_data = [CommandFlag.START, *padded_data, command, CommandFlag.END]
    self.send_bytes(raw_data)

  def send_bytes(self, data):
    """
    Helper method to send raw bytes directly to the controller

    Mostly for internal use, prefer the specific functions where possible
    """
    raw_data = bytes(data)

    attempts = 0
    while True:
      try:
        self.sock.sendall(raw_data)
        return
      except (socket.timeout, BrokenPipeError):
        if (attempts < self.retries):
          self.reconnect()
          attempts += 1
        else:
          raise

  def sync_state(self):
    """
    Syncs the state of the controller with the state of this object
    """
    attempts = 0
    while True:
      try:
        # Send the request for sync data
        self.send_command(Command.SYNC)

        response = self.sock.recv(1024)

        # Extract the state data
        state = bytearray(response)
        self.state.update_from_sync(state)
        return
      except (socket.timeout, BrokenPipeError):
        # When there is an error with the socket, close the connection and connect again
        if attempts < self.retries:
          self.reconnect()
          attempts += 1
        else:
          raise

  def __repr__(self):
    return f"""WikiLedShopLight @ {self.ip}:{self.port}
      state: {self.state}
    """

A Wifi LED Shop Light Creates a new Wifi LED Shop light      :param ip: The IP of the controller on the network (STA Mode, not AP mode).     :param port: The port the controller should listen on. It should almost always be left as the default.     :param timeout: The timeout in seconds to wait listening to the socket.     :param retries: The number of times to retry sending a command if it fails or times out before giving up. Try to (re-)connect to the controller via a socket Closes the socket connection to the light Sets the color of the light (rgb each 0 to 255) Sets the brightness of the light      :param brightness: An int describing the brightness (0 to 255, where 255 is the brightest) Sets the speed of the effect. Not all effects use the speed, but it can be safely set regardless          :param speed: An int describing the speed an effect will play at. (0 to 255, where 255 is the fastest) Sets the light effect to the provided built-in effect number      :param preset: The preset effect to use. Valid values are 0 to 255. See the MonoEffect enum, or MONO_EFFECTS and PRESET_EFFECTS for mapping. Sets the light effect to the provided custom effect number          :param custom: The custom effect to use. Valid values are 1 to 12. See the CustomEffect enum. Toggles the state of the light without checking the current state Toggles the light on only if it is not already on Toggles the light off only if it is not already off Sets the total number of segments. Total lights is segments * lights_per_segment.          :param segments: The number of segments Sets the number of lights per segment. Total lights is segments * lights_per_segment.      :param lights_per_segment: The number of lights per segment Helper function to automatically set the number of segments and lights per segment     to reach the target total lights (rounded down to never exceed total_lights)      Usually you know the total number of lights you have available on a light strip     and want to split it into segments that take up the whole strip      :param total_lights: The target total number of lights to use     :param segments: The number of segments to split the total into Helper method to send a command to the controller.      Mostly for internal use, prefer the specific functions where possible.          Formats the low level message details like Start/End flag, binary data, and command      :param command: The command to send to the controller. See the Command enum for valid commands. Helper method to send raw bytes directly to the controller      Mostly for internal use, prefer the specific functions where possible Syncs the state of the controller with the state of this object # Send the request for sync data # Extract the state data # When there is an error with the socket, close the connection and connect again WikiLedShopLight @ {self.ip}:{self.port}       state: {self.state}