import bisect
import configparser
import os
import re

from i3pystatus import IntervalModule, formatp
from i3pystatus.core.command import run_through_shell
from i3pystatus.core.desktop import DesktopNotification
from i3pystatus.core.util import lchop, TimeWrapper, make_bar, make_glyph, make_vertical_bar


class UEventParser(configparser.ConfigParser):
    @staticmethod
    def parse_file(file):
        parser = UEventParser()
        with open(file, "r") as file:
            parser.read_string(file.read())
        return dict(parser.items("id10t"))

    def __init__(self):
        super().__init__(default_section="id10t")

    def optionxform(self, key):
        return lchop(key, "POWER_SUPPLY_")

    def read_string(self, string):
        super().read_string("[id10t]\n" + string)


class Battery:
    @staticmethod
    def create(from_file):
        battery_info = UEventParser.parse_file(from_file)
        if "POWER_NOW" in battery_info:
            return BatteryEnergy(battery_info)
        else:
            return BatteryCharge(battery_info)

    def __init__(self, battery_info):
        self.battery_info = battery_info
        self.normalize_micro()

    def normalize_micro(self):
        for key, micro_value in self.battery_info.items():
            if re.match(r"(VOLTAGE|CHARGE|CURRENT|POWER|ENERGY)_(NOW|FULL|MIN)(_DESIGN)?", key):
                self.battery_info[key] = float(micro_value) / 1000000.0

    def percentage(self, design=False):
        return self._percentage("_DESIGN" if design else "") * 100

    def status(self):
        if self.consumption() is None:
            return self.battery_info["STATUS"]
        elif self.consumption() > 0.1 and self.percentage() < 99.9:
            return "Discharging" if self.battery_info["STATUS"] == "Discharging" else "Charging"
        elif self.consumption() == 0 and self.percentage() == 0.00:
            return "Depleted"
        else:
            return "Full"

    def consumption(self, val):
        return val if val > 0.1 else 0


class BatteryCharge(Battery):
    def __init__(self, bi):
        bi["CHARGE_FULL"] = bi["CHARGE_FULL_DESIGN"] if bi["CHARGE_NOW"] > bi["CHARGE_FULL"] else bi["CHARGE_FULL"]
        super().__init__(bi)

    def consumption(self):
        if "VOLTAGE_NOW" in self.battery_info and "CURRENT_NOW" in self.battery_info:
            return super().consumption(self.battery_info["VOLTAGE_NOW"] * abs(self.battery_info["CURRENT_NOW"]))  # V * A = W
        else:
            return None

    def _percentage(self, design):
        return self.battery_info["CHARGE_NOW"] / self.battery_info["CHARGE_FULL" + design]

    def wh_remaining(self):
        return self.battery_info['CHARGE_NOW'] * self.battery_info['VOLTAGE_NOW']

    def wh_total(self):
        return self.battery_info['CHARGE_FULL'] * self.battery_info['VOLTAGE_NOW']

    def wh_depleted(self):
        return (self.battery_info['CHARGE_FULL'] - self.battery_info['CHARGE_NOW']) * self.battery_info['VOLTAGE_NOW']

    def remaining(self):
        if self.status() == "Discharging":
            if "CHARGE_NOW" in self.battery_info and "CURRENT_NOW" in self.battery_info:
                # Ah / A = h * 60 min = min
                return self.battery_info["CHARGE_NOW"] / self.battery_info["CURRENT_NOW"] * 60
            else:
                return -1
        else:
            return (self.battery_info["CHARGE_FULL"] - self.battery_info["CHARGE_NOW"]) / self.battery_info[
                "CURRENT_NOW"] * 60


class BatteryEnergy(Battery):
    def consumption(self):
        return super().consumption(self.battery_info["POWER_NOW"])

    def _percentage(self, design):
        return self.battery_info["ENERGY_NOW"] / self.battery_info["ENERGY_FULL" + design]

    def wh_remaining(self):
        return self.battery_info['ENERGY_NOW']

    def wh_total(self):
        return self.battery_info['ENERGY_FULL']

    def wh_depleted(self):
        return self.battery_info['ENERGY_FULL'] - self.battery_info['ENERGY_NOW']

    def remaining(self):
        if self.status() == "Discharging":
            # Wh / W = h * 60 min = min
            return self.battery_info["ENERGY_NOW"] / self.battery_info["POWER_NOW"] * 60
        else:
            return (self.battery_info["ENERGY_FULL"] - self.battery_info["ENERGY_NOW"]) / self.battery_info[
                "POWER_NOW"] * 60


class BatteryChecker(IntervalModule):
    """
    This class uses the /sys/class/power_supply/…/uevent interface to check for the
    battery status.

    Setting ``battery_ident`` to ``ALL`` will summarise all available batteries
    and aggregate the % as well as the time remaining on the charge. This is
    helpful when the machine has more than one battery available.

    .. rubric:: Available formatters

    * `{remaining}` — remaining time for charging or discharging, uses TimeWrapper formatting, default format is `%E%h:%M`
    * `{percentage}` — battery percentage relative to the last full value
    * `{percentage_design}` — absolute battery charge percentage
    * `{consumption (Watts)}` — current power flowing into/out of the battery
    * `{status}`
    * `{no_of_batteries}` — The number of batteries included
    * `{battery_ident}` — the same as the setting
    * `{bar}` —bar displaying the relative percentage graphically
    * `{bar_design}` —bar displaying the absolute percentage graphically
    * `{glyph}` — A single character or string (selected from 'glyphs') representing the current battery percentage

    This module supports the :ref:`formatp <formatp>` extended string format
    syntax. By setting the ``FULL`` status to an empty string, and including
    brackets around the ``{status}`` formatter, the text within the brackets
    will be hidden when the battery is full, as can be seen in the below
    example:

    .. code-block:: python

        from i3pystatus import Status

        status = Status()

        status.register(
            'battery',
            interval=5,
            format='{battery_ident}: [{status} ]{percentage_design:.2f}%',
            alert=True,
            alert_percentage=15,
            status={
                'DPL': 'DPL',
                'CHR': 'CHR',
                'DIS': 'DIS',
                'FULL': '',
            }
        )

        # status.register(
        #     'battery',
        #     format='{status} {percentage:.0f}%',
        #     levels={
        #         25: "<=25",
        #         50: "<=50",
        #         75: "<=75",
        #     },
        # )

        status.run()

    """

    settings = (
        ("battery_ident", "The name of your battery, usually BAT0 or BAT1"),
        "format",
        ("not_present_text", "Text displayed if the battery is not present. No formatters are available"),
        ("alert", "Display a libnotify-notification on low battery"),
        ("critical_level_command", "Runs a shell command in the case of a critical power state"),
        "critical_level_percentage",
        "alert_percentage",
        "alert_timeout",
        ("alert_format_title", "The title of the notification, all formatters can be used"),
        ("alert_format_body", "The body text of the notification, all formatters can be used"),
        ("path", "Override the default-generated path and specify the full path for a single battery"),
        ("base_path", "Override the default base path for searching for batteries"),
        ("battery_prefix", "Override the default battery prefix"),
        ("status", "A dictionary mapping ('DPL', 'DIS', 'CHR', 'FULL') to alternative names"),
        ("levels", "A dictionary mapping percentages of charge levels to corresponding names."),
        ("color", "The text color"),
        ("full_color", "The full color"),
        ("charging_color", "The charging color"),
        ("critical_color", "The critical color"),
        ("not_present_color", "The not present color."),
        ("not_present_text",
         "The text to display when the battery is not present. Provides {battery_ident} as formatting option"),
        ("no_text_full", "Don't display text when battery is full - 100%"),
        ("glyphs", "Arbitrarily long string of characters (or array of strings) to represent battery charge percentage"),
    )

    battery_ident = "ALL"
    format = "{status} {remaining}"
    status = {
        "DPL": "DPL",
        "CHR": "CHR",
        "DIS": "DIS",
        "FULL": "FULL",
    }
    levels = None
    not_present_text = "Battery {battery_ident} not present"

    alert = False
    critical_level_command = ""
    critical_level_percentage = 1
    alert_percentage = 10
    alert_timeout = -1
    alert_format_title = "Low battery"
    alert_format_body = "Battery {battery_ident} has only {percentage:.2f}% ({remaining:%E%hh:%Mm}) remaining!"
    color = "#ffffff"
    full_color = "#00ff00"
    charging_color = "#00ff00"
    critical_color = "#ff0000"
    not_present_color = "#ffffff"
    no_text_full = False
    glyphs = "▁▂▃▄▅▆▇█"

    battery_prefix = 'BAT'
    base_path = '/sys/class/power_supply'
    path = None
    paths = []

    notification = None

    def percentage(self, batteries, design=False):
        total_now = [battery.wh_remaining() for battery in batteries]
        total_full = [battery.wh_total() for battery in batteries]
        return sum(total_now) / sum(total_full) * 100

    def consumption(self, batteries):
        consumption = 0
        for battery in batteries:
            if battery.consumption() is not None:
                consumption += battery.consumption()
        return consumption

    def abs_consumption(self, batteries):
        abs_consumption = 0
        for battery in batteries:
            if battery.consumption() is None:
                continue
            if battery.status() == 'Discharging':
                abs_consumption -= battery.consumption()
            elif battery.status() == 'Charging':
                abs_consumption += battery.consumption()
        return abs_consumption

    def battery_status(self, batteries):
        abs_consumption = self.abs_consumption(batteries)
        if abs_consumption > 0:
            return 'Charging'
        elif abs_consumption < 0:
            return 'Discharging'
        else:
            return batteries[-1].status()

    def remaining(self, batteries):
        wh_depleted = 0
        wh_remaining = 0
        abs_consumption = self.abs_consumption(batteries)
        for battery in batteries:
            wh_remaining += battery.wh_remaining()
            wh_depleted += battery.wh_depleted()
        if abs_consumption == 0:
            return 0
        elif abs_consumption > 0:
            return wh_depleted / self.consumption(batteries) * 60
        elif abs_consumption < 0:
            return wh_remaining / self.consumption(batteries) * 60

    def init(self):
        if not self.paths or (self.path and self.path not in self.paths):
            bat_dir = self.base_path
            if os.path.exists(bat_dir) and not self.path:
                _, dirs, _ = next(os.walk(bat_dir))
                all_bats = [x for x in dirs if x.startswith(self.battery_prefix)]
                for bat in all_bats:
                    self.paths.append(os.path.join(bat_dir, bat, 'uevent'))
            if self.path:
                self.paths = [self.path]

    def run(self):
        urgent = False
        color = self.color
        batteries = []

        for path in self.paths:
            if self.battery_ident == 'ALL' or path.find(self.battery_ident) >= 0:
                try:
                    batteries.append(Battery.create(path))
                except FileNotFoundError:
                    pass

        if not batteries:
            format_dict = {'battery_ident': self.battery_ident}
            self.output = {
                "full_text": formatp(self.not_present_text, **format_dict),
                "color": self.not_present_color,
            }
            return
        if self.no_text_full:
            if self.battery_status(batteries) == "Full":
                self.output = {
                    "full_text": ""
                }
                return

        fdict = {
            "battery_ident": self.battery_ident,
            "no_of_batteries": len(batteries),
            "percentage": self.percentage(batteries),
            "percentage_design": self.percentage(batteries, design=True),
            "consumption": self.consumption(batteries),
            "remaining": TimeWrapper(0, "%E%h:%M"),
            "glyph": make_glyph(self.percentage(batteries), self.glyphs),
            "bar": make_bar(self.percentage(batteries)),
            "bar_design": make_bar(self.percentage(batteries, design=True)),
            "vertical_bar": make_vertical_bar(self.percentage(batteries)),
            "vertical_bar_design": make_vertical_bar(self.percentage(batteries, design=True)),
        }

        status = self.battery_status(batteries)
        if status in ["Charging", "Discharging"]:
            remaining = self.remaining(batteries)
            fdict["remaining"] = TimeWrapper(remaining * 60, "%E%h:%M")
            if status == "Discharging":
                fdict["status"] = "DIS"
                if self.percentage(batteries) <= self.alert_percentage:
                    urgent = True
                    color = self.critical_color
            else:
                fdict["status"] = "CHR"
                color = self.charging_color
        elif status == 'Depleted':
            fdict["status"] = "DPL"
            color = self.critical_color
        else:
            fdict["status"] = "FULL"
            color = self.full_color
        if self.critical_level_command and fdict["status"] == "DIS" and fdict["percentage"] <= self.critical_level_percentage:
            run_through_shell(self.critical_level_command, enable_shell=True)

        if self.alert and fdict["status"] == "DIS" and fdict["percentage"] <= self.alert_percentage:
            title, body = formatp(self.alert_format_title, **fdict), formatp(self.alert_format_body, **fdict)
            if self.notification is None:
                self.notification = DesktopNotification(
                    title=title,
                    body=body,
                    icon="battery-caution",
                    urgency=2,
                    timeout=self.alert_timeout,
                )
                self.notification.display()
            else:
                self.notification.update(title=title,
                                         body=body)

        if self.levels and fdict['status'] == 'DIS':
            self.levels.setdefault(0, self.status.get('DPL', 'DPL'))
            self.levels.setdefault(100, self.status.get('FULL', 'FULL'))
            keys = sorted(self.levels.keys())
            index = bisect.bisect_left(keys, int(fdict['percentage']))
            fdict["status"] = self.levels[keys[index]]
        else:
            fdict["status"] = self.status[fdict["status"]]

        self.data = fdict
        self.output = {
            "full_text": formatp(self.format, **fdict),
            "instance": self.battery_ident,
            "urgent": urgent,
            "color": color,
        }
