#!/usr/bin/env python3

# Standard library imports
import argparse
import collections
import enum
import logging
import logging.handlers
import re
import subprocess
import sys

_LOGGER = logging.getLogger(__name__)
_LOGGER.setLevel(logging.INFO)
_LOGGER.addHandler(logging.handlers.SysLogHandler("/dev/log"))


class _State(str, enum.Enum):
    DONE = "DONE"
    INIT = "INIT"
    NO_OUTPUTS = "NO_OUTPUTS"
    OUTPUT = "OUTPUT"
    OUTPUT_MODE = "OUTPUT_MODE"
    OUTPUT_PROP = "OUTPUT_PROP"

    def __str__(self):
        return self.value

    def __repr__(self):
        return repr(self.value)


class _TokenId(str, enum.Enum):
    CONNECTOR = "CONNECTOR"
    EOF = "EOF"
    MODE = "MODE"
    PROP_VALUE_CONTD = "PROP_VALUE_CONTD"
    PROP_ATTR_RANGE = "PROP_ATTR_RANGE"
    PROP_ATTR_SUPPORTED = "PROP_ATTR_SUPPORTED"
    PROP_HEAD = "PROP_HEAD"
    SCREEN = "SCREEN"

    def __str__(self):
        return self.value

    def __repr__(self):
        return repr(self.value)


_TOKEN_REGEXES = collections.OrderedDict(
    (
        (
            _TokenId.CONNECTOR,
            r"^(?P<name>[^\s]+) (?P<state>connected|disconnected) .*$",
        ),
        (
            _TokenId.EOF,
            r"^$",
        ),
        (
            _TokenId.MODE,
            # valid lines here are similar to these:
            #   "   1920x1080     60.01*+  60.01    59.97    59.96    59.93"
            #   "   1680x1050     59.95    59.88"
            # but also
            #   "  3840x2160 (0xcc) 529.200MHz +HSync +VSync"
            # is a possible option.
            r"^\s+(?P<resolution>\d+x\d+[ip]?) (?P<rates>.*?)\s*$",
        ),
        (
            _TokenId.PROP_ATTR_RANGE,
            r"^\t\trange: \((?P<value_min>\d+), (?P<value_max>\d+)\).*$",
        ),
        (
            _TokenId.PROP_ATTR_SUPPORTED,
            r"^\t\tsupported: (?P<supported_values>.*?)\s*$",
        ),
        (
            _TokenId.PROP_VALUE_CONTD,
            r"^\t\t(?P<value>.*?)\s*$",
        ),
        (
            _TokenId.PROP_HEAD,
            r"^\t(?P<name>[^:]+): (?P<value>.*?)\s*$",
        ),
        (
            _TokenId.SCREEN,
            r"^Screen (?P<number>\d+):.*$",
        ),
    )
)


def _tokenize(line):
    for token_id, token_regex in _TOKEN_REGEXES.items():
        token_match = re.match(token_regex, line)
        if token_match is not None:
            return token_id, token_match.groupdict()
    raise ValueError("invalid line", line)


class XRandrPropOutputParser:
    def __init__(self):
        self.__transitions = {
            # (Current state, Input token): (Action, Next state)
            (_State.INIT, _TokenId.SCREEN): (None, _State.NO_OUTPUTS),
            (_State.NO_OUTPUTS, _TokenId.CONNECTOR): (
                self.__action_create_output,
                _State.OUTPUT,
            ),
            (_State.OUTPUT, _TokenId.PROP_HEAD): (
                self.__action_create_prop,
                _State.OUTPUT_PROP,
            ),
            (_State.OUTPUT_PROP, _TokenId.PROP_VALUE_CONTD): (
                self.__action_append_prop_value,
                _State.OUTPUT_PROP,
            ),
            (_State.OUTPUT_PROP, _TokenId.PROP_ATTR_RANGE): (
                self.__action_add_prop_attr_range,
                _State.OUTPUT_PROP,
            ),
            (_State.OUTPUT_PROP, _TokenId.PROP_ATTR_SUPPORTED): (
                self.__action_add_prop_attr_supported,
                _State.OUTPUT_PROP,
            ),
            (_State.OUTPUT_PROP, _TokenId.PROP_HEAD): (
                self.__action_create_prop,
                _State.OUTPUT_PROP,
            ),
            (_State.OUTPUT_PROP, _TokenId.CONNECTOR): (
                self.__action_create_output,
                _State.OUTPUT,
            ),
            (_State.OUTPUT_PROP, _TokenId.MODE): (None, _State.OUTPUT_MODE),
            (_State.OUTPUT_PROP, _TokenId.EOF): (None, _State.DONE),
            (_State.OUTPUT_MODE, _TokenId.MODE): (None, _State.OUTPUT_MODE),
            (_State.OUTPUT_MODE, _TokenId.CONNECTOR): (
                self.__action_create_output,
                _State.OUTPUT,
            ),
            (_State.OUTPUT_MODE, _TokenId.EOF): (None, _State.DONE),
        }
        self.__current_state = _State.INIT
        self.__displays = {}
        self.__last_output = None
        self.__last_prop = None

    def __action_create_output(self, token_id, *, name, state):
        if name in self.__displays:
            raise RuntimeError("output is already defined", name)
        self.__displays[name] = self.__last_output = {"name": name, "state": state}

    def __action_create_prop(self, token_id, *, name, value):
        self.__last_output.setdefault("props", {})[name] = self.__last_prop = {
            "name": name,
            "value": value,
        }

    def __action_append_prop_value(self, token_id, *, value):
        self.__last_prop["value"] += value

    def __action_add_prop_attr_range(self, token_id, *, value_min, value_max):
        # Because this property has range attribute, it must be int.
        self.__last_prop["value"] = int(self.__last_prop["value"], 10)
        self.__last_prop["value_min"] = int(value_min, 10)
        self.__last_prop["value_max"] = int(value_max, 10)

    def __action_add_prop_attr_supported(self, token_id, supported_values):
        self.__last_prop["supported_values"] = [
            v.strip() for v in supported_values.split(",")
        ]

    def __push(self, token_id, token_groupdict):
        action, next_state = self.__transitions[(self.__current_state, token_id)]
        if action is not None:
            action(token_id, **token_groupdict)
        self.__current_state = next_state

    def parse(self, xrandr_prop_output: str) -> dict:
        for line in xrandr_prop_output.splitlines():
            try:
              token_id, token_groupdict = _tokenize(line)
              self.__push(token_id, token_groupdict)
            except Exception as e:
              _LOGGER.warning("could not tokenize line: %s", e)

        self.__push(_TokenId.EOF, "")

        return self.__displays


def _xrandr_get_prop() -> dict:
    xrandr_prop_output = subprocess.check_output(["xrandr", "--prop"]).decode("utf-8")
    xrandr_prop_output_parser = XRandrPropOutputParser()

    return xrandr_prop_output_parser.parse(xrandr_prop_output)


def _xrandr_set_max_bpc(output_name: str, max_bpc: int):
    subprocess.check_call(
        [
            "xrandr",
            "--output",
            output_name,
            "--set",
            "max bpc",
            str(max_bpc),
        ]
    )


def _main():
    puavo_displays_max_bpc_str = (
        subprocess.check_output(["puavo-conf", "puavo.displays.max_bpc"])
        .decode("utf-8")
        .strip()
    )

    argparser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description="Set max bpc of all displays.",
    )
    argparser.add_argument(
        "MAX_BPC",
        nargs="?",
        help="If not given, puavo.displays.max_bpc value is used instead.",
        default=puavo_displays_max_bpc_str,
    )
    args = argparser.parse_args()

    try:
        desired_max_bpc = int(args.MAX_BPC, 10)
    except ValueError:
        _LOGGER.error("invalid max bpc %r, expected 10base integer", args.MAX_BPC)
        sys.exit(1)

    for output_name, output in _xrandr_get_prop().items():
        if "max bpc" in output["props"]:
            _LOGGER.info("desired max bpc of %r is %d", output_name, desired_max_bpc)

            max_bpc_prop = output["props"]["max bpc"]
            current_value = max_bpc_prop["value"]
            value_min = max_bpc_prop["value_min"]
            value_max = max_bpc_prop["value_max"]

            new_value = min(max(value_min, desired_max_bpc), value_max)
            if new_value != desired_max_bpc:
                _LOGGER.info(
                    "adjusted desired max bpc of %r from %d to %d to "
                    "match the supported range (%d, %d)",
                    output_name,
                    desired_max_bpc,
                    new_value,
                    value_min,
                    value_max,
                )

            _xrandr_set_max_bpc(output_name, new_value)
            _LOGGER.info(
                "set max bpc of %r from %d to %d",
                output_name,
                current_value,
                new_value,
            )


if __name__ == "__main__":
    _main()
