import binascii
import itertools
import os
import re
import struct
import subprocess

from cereal import log
from selfdrive.hardware.base import HardwareBase, ThermalConfig

NetworkType = log.DeviceState.NetworkType
NetworkStrength = log.DeviceState.NetworkStrength


def service_call(call):
  try:
    ret = subprocess.check_output(["service", "call", *call], encoding='utf8').strip()
    if 'Parcel' not in ret:
      return None
    return parse_service_call_bytes(ret)
  except subprocess.CalledProcessError:
    return None


def parse_service_call_unpack(r, fmt):
  try:
    return struct.unpack(fmt, r)[0]
  except Exception:
    return None


def parse_service_call_string(r):
  try:
    r = r[8:]  # Cut off length field
    r = r.decode('utf_16_be')

    # All pairs of two characters seem to be swapped. Not sure why
    result = ""
    for a, b, in itertools.zip_longest(r[::2], r[1::2], fillvalue='\x00'):
        result += b + a

    result = result.replace('\x00', '')

    return result
  except Exception:
    return None


def parse_service_call_bytes(ret):
  try:
    r = b""
    for hex_part in re.findall(r'[ (]([0-9a-f]{8})', ret):
      r += binascii.unhexlify(hex_part)
    return r
  except Exception:
    return None


def getprop(key):
  return subprocess.check_output(["getprop", key], encoding='utf8').strip()


class Android(HardwareBase):
  def get_os_version(self):
    with open("/VERSION") as f:
      return f.read().strip()

  def get_device_type(self):
    return "eon"

  def get_sound_card_online(self):
    return (os.path.isfile('/proc/asound/card0/state') and
            open('/proc/asound/card0/state').read().strip() == 'ONLINE')

  def get_imei(self, slot):
    slot = str(slot)
    if slot not in ("0", "1"):
      raise ValueError("SIM slot must be 0 or 1")

    return parse_service_call_string(service_call(["iphonesubinfo", "3", "i32", str(slot)]))

  def get_serial(self):
    ret = getprop("ro.serialno")
    if ret == "":
      ret = "cccccccc"
    return ret

  def get_subscriber_info(self):
    ret = parse_service_call_string(service_call(["iphonesubinfo", "7"]))
    if ret is None or len(ret) < 8:
      return ""
    return ret

  def reboot(self, reason=None):
    # e.g. reason="recovery" to go into recover mode
    if reason is None:
      reason_args = ["null"]
    else:
      reason_args = ["s16", reason]

    subprocess.check_output([
      "service", "call", "power", "16",  # IPowerManager.reboot
      "i32", "0",  # no confirmation,
      *reason_args,
      "i32", "1"  # wait
    ])

  def uninstall(self):
    with open('/cache/recovery/command', 'w') as f:
      f.write('--wipe_data\n')
    # IPowerManager.reboot(confirm=false, reason="recovery", wait=true)
    self.reboot(reason="recovery")

  def get_sim_info(self):
    # Used for athena
    # TODO: build using methods from this class
    sim_state = getprop("gsm.sim.state").split(",")
    network_type = getprop("gsm.network.type").split(',')
    mcc_mnc = getprop("gsm.sim.operator.numeric") or None

    sim_id = parse_service_call_string(service_call(['iphonesubinfo', '11']))
    cell_data_state = parse_service_call_unpack(service_call(['phone', '46']), ">q")
    cell_data_connected = (cell_data_state == 2)

    return {
      'sim_id': sim_id,
      'mcc_mnc': mcc_mnc,
      'network_type': network_type,
      'sim_state': sim_state,
      'data_connected': cell_data_connected
    }

  def get_network_type(self):
    wifi_check = parse_service_call_string(service_call(["connectivity", "2"]))
    if wifi_check is None:
      return NetworkType.none
    elif 'WIFI' in wifi_check:
      return NetworkType.wifi
    else:
      cell_check = parse_service_call_unpack(service_call(['phone', '59']), ">q")
      # from TelephonyManager.java
      cell_networks = {
        0: NetworkType.none,
        1: NetworkType.cell2G,
        2: NetworkType.cell2G,
        3: NetworkType.cell3G,
        4: NetworkType.cell2G,
        5: NetworkType.cell3G,
        6: NetworkType.cell3G,
        7: NetworkType.cell3G,
        8: NetworkType.cell3G,
        9: NetworkType.cell3G,
        10: NetworkType.cell3G,
        11: NetworkType.cell2G,
        12: NetworkType.cell3G,
        13: NetworkType.cell4G,
        14: NetworkType.cell4G,
        15: NetworkType.cell3G,
        16: NetworkType.cell2G,
        17: NetworkType.cell3G,
        18: NetworkType.cell4G,
        19: NetworkType.cell4G
      }
      return cell_networks.get(cell_check, NetworkType.none)

  def get_network_strength(self, network_type):
    network_strength = NetworkStrength.unknown

    # from SignalStrength.java
    def get_lte_level(rsrp, rssnr):
      INT_MAX = 2147483647
      if rsrp == INT_MAX:
        lvl_rsrp = NetworkStrength.unknown
      elif rsrp >= -95:
        lvl_rsrp = NetworkStrength.great
      elif rsrp >= -105:
        lvl_rsrp = NetworkStrength.good
      elif rsrp >= -115:
        lvl_rsrp = NetworkStrength.moderate
      else:
        lvl_rsrp = NetworkStrength.poor
      if rssnr == INT_MAX:
        lvl_rssnr = NetworkStrength.unknown
      elif rssnr >= 45:
        lvl_rssnr = NetworkStrength.great
      elif rssnr >= 10:
        lvl_rssnr = NetworkStrength.good
      elif rssnr >= -30:
        lvl_rssnr = NetworkStrength.moderate
      else:
        lvl_rssnr = NetworkStrength.poor
      return max(lvl_rsrp, lvl_rssnr)

    def get_tdscdma_level(tdscmadbm):
      lvl = NetworkStrength.unknown
      if tdscmadbm > -25:
        lvl = NetworkStrength.unknown
      elif tdscmadbm >= -49:
        lvl = NetworkStrength.great
      elif tdscmadbm >= -73:
        lvl = NetworkStrength.good
      elif tdscmadbm >= -97:
        lvl = NetworkStrength.moderate
      elif tdscmadbm >= -110:
        lvl = NetworkStrength.poor
      return lvl

    def get_gsm_level(asu):
      if asu <= 2 or asu == 99:
        lvl = NetworkStrength.unknown
      elif asu >= 12:
        lvl = NetworkStrength.great
      elif asu >= 8:
        lvl = NetworkStrength.good
      elif asu >= 5:
        lvl = NetworkStrength.moderate
      else:
        lvl = NetworkStrength.poor
      return lvl

    def get_evdo_level(evdodbm, evdosnr):
      lvl_evdodbm = NetworkStrength.unknown
      lvl_evdosnr = NetworkStrength.unknown
      if evdodbm >= -65:
        lvl_evdodbm = NetworkStrength.great
      elif evdodbm >= -75:
        lvl_evdodbm = NetworkStrength.good
      elif evdodbm >= -90:
        lvl_evdodbm = NetworkStrength.moderate
      elif evdodbm >= -105:
        lvl_evdodbm = NetworkStrength.poor
      if evdosnr >= 7:
        lvl_evdosnr = NetworkStrength.great
      elif evdosnr >= 5:
        lvl_evdosnr = NetworkStrength.good
      elif evdosnr >= 3:
        lvl_evdosnr = NetworkStrength.moderate
      elif evdosnr >= 1:
        lvl_evdosnr = NetworkStrength.poor
      return max(lvl_evdodbm, lvl_evdosnr)

    def get_cdma_level(cdmadbm, cdmaecio):
      lvl_cdmadbm = NetworkStrength.unknown
      lvl_cdmaecio = NetworkStrength.unknown
      if cdmadbm >= -75:
        lvl_cdmadbm = NetworkStrength.great
      elif cdmadbm >= -85:
        lvl_cdmadbm = NetworkStrength.good
      elif cdmadbm >= -95:
        lvl_cdmadbm = NetworkStrength.moderate
      elif cdmadbm >= -100:
        lvl_cdmadbm = NetworkStrength.poor
      if cdmaecio >= -90:
        lvl_cdmaecio = NetworkStrength.great
      elif cdmaecio >= -110:
        lvl_cdmaecio = NetworkStrength.good
      elif cdmaecio >= -130:
        lvl_cdmaecio = NetworkStrength.moderate
      elif cdmaecio >= -150:
        lvl_cdmaecio = NetworkStrength.poor
      return max(lvl_cdmadbm, lvl_cdmaecio)

    if network_type == NetworkType.none:
      return network_strength
    if network_type == NetworkType.wifi:
      out = subprocess.check_output('dumpsys connectivity', shell=True).decode('utf-8')
      network_strength = NetworkStrength.unknown
      for line in out.split('\n'):
        signal_str = "SignalStrength: "
        if signal_str in line:
          lvl_idx_start = line.find(signal_str) + len(signal_str)
          lvl_idx_end = line.find(']', lvl_idx_start)
          lvl = int(line[lvl_idx_start : lvl_idx_end])
          if lvl >= -50:
            network_strength = NetworkStrength.great
          elif lvl >= -60:
            network_strength = NetworkStrength.good
          elif lvl >= -70:
            network_strength = NetworkStrength.moderate
          else:
            network_strength = NetworkStrength.poor
      return network_strength
    else:
      # check cell strength
      out = subprocess.check_output('dumpsys telephony.registry', shell=True).decode('utf-8')
      for line in out.split('\n'):
        if "mSignalStrength" in line:
          arr = line.split(' ')
          ns = 0
          if ("gsm" in arr[14]):
            rsrp = int(arr[9])
            rssnr = int(arr[11])
            ns = get_lte_level(rsrp, rssnr)
            if ns == NetworkStrength.unknown:
              tdscmadbm = int(arr[13])
              ns = get_tdscdma_level(tdscmadbm)
              if ns == NetworkStrength.unknown:
                asu = int(arr[1])
                ns = get_gsm_level(asu)
          else:
            cdmadbm = int(arr[3])
            cdmaecio = int(arr[4])
            evdodbm = int(arr[5])
            evdosnr = int(arr[7])
            lvl_cdma = get_cdma_level(cdmadbm, cdmaecio)
            lvl_edmo = get_evdo_level(evdodbm, evdosnr)
            if lvl_edmo == NetworkStrength.unknown:
              ns = lvl_cdma
            elif lvl_cdma == NetworkStrength.unknown:
              ns = lvl_edmo
            else:
              ns = min(lvl_cdma, lvl_edmo)
          network_strength = max(network_strength, ns)

      return network_strength

  def get_battery_capacity(self):
    return self.read_param_file("/sys/class/power_supply/battery/capacity", int, 100)

  def get_battery_status(self):
    # This does not correspond with actual charging or not.
    # If a USB cable is plugged in, it responds with 'Charging', even when charging is disabled
    return self.read_param_file("/sys/class/power_supply/battery/status", lambda x: x.strip(), '')

  def get_battery_current(self):
    return self.read_param_file("/sys/class/power_supply/battery/current_now", int)

  def get_battery_voltage(self):
    return self.read_param_file("/sys/class/power_supply/battery/voltage_now", int)

  def get_battery_charging(self):
    # This does correspond with actually charging
    return self.read_param_file("/sys/class/power_supply/battery/charge_type", lambda x: x.strip() != "N/A", True)

  def set_battery_charging(self, on):
    with open('/sys/class/power_supply/battery/charging_enabled', 'w') as f:
      f.write(f"{1 if on else 0}\n")

  def get_usb_present(self):
    return self.read_param_file("/sys/class/power_supply/usb/present", lambda x: bool(int(x)), False)

  def get_current_power_draw(self):
    # We don't have a good direct way to measure this on android
    return None

  def shutdown(self):
    os.system('LD_LIBRARY_PATH="" svc power shutdown')

  def get_thermal_config(self):
    return ThermalConfig(cpu=((5, 7, 10, 12), 10), gpu=((16,), 10), mem=(2, 10), bat=(29, 1000), ambient=(25, 1))

  def set_screen_brightness(self, percentage):
    with open("/sys/class/leds/lcd-backlight/brightness", "w") as f:
      f.write(str(int(percentage * 2.55)))
