{ config, lib, pkgs, ... }:

with lib;
with builtins;

let

  cfg = config.services.compton;

  pairOf = x: with types; addCheck (listOf x) (y: length y == 2);

  floatBetween = a: b: with lib; with types;
    addCheck str (x: versionAtLeast x a && versionOlder x b);

  toConf = attrs: concatStringsSep "\n"
    (mapAttrsToList
      (k: v: let
        sep = if isAttrs v then ":" else "=";
        # Basically a tinkered lib.generators.mkKeyValueDefault
        mkValueString = v:
          if isBool v        then boolToString v
          else if isInt v    then toString v
          else if isFloat v  then toString v
          else if isString v then ''"${escape [ ''"'' ] v}"''
          else if isList v   then "[ "
            + concatMapStringsSep " , " mkValueString v
            + " ]"
          else if isAttrs v  then "{ "
            + concatStringsSep " "
              (mapAttrsToList
                (key: value: "${toString key}=${mkValueString value};")
                v)
            + " }"
          else abort "compton.mkValueString: unexpected type (v = ${v})";
      in "${escape [ sep ] k}${sep}${mkValueString v};")
      attrs);

  configFile = pkgs.writeText "compton.conf" (toConf cfg.settings);

in {

  options.services.compton = {
    enable = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Whether of not to enable Compton as the X.org composite manager.
      '';
    };

    fade = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Fade windows in and out.
      '';
    };

    fadeDelta = mkOption {
      type = types.addCheck types.int (x: x > 0);
      default = 10;
      example = 5;
      description = ''
        Time between fade animation step (in ms).
      '';
    };

    fadeSteps = mkOption {
      type = pairOf (floatBetween "0.01" "1.01");
      default = [ "0.028" "0.03" ];
      example = [ "0.04" "0.04" ];
      description = ''
        Opacity change between fade steps (in and out).
        (numbers in range 0.01 - 1.0)
      '';
    };

    fadeExclude = mkOption {
      type = types.listOf types.str;
      default = [];
      example = [
        "window_type *= 'menu'"
        "name ~= 'Firefox$'"
        "focused = 1"
      ];
      description = ''
        List of conditions of windows that should not be faded.
        See <literal>compton(1)</literal> man page for more examples.
      '';
    };

    shadow = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Draw window shadows.
      '';
    };

    shadowOffsets = mkOption {
      type = pairOf types.int;
      default = [ (-15) (-15) ];
      example = [ (-10) (-15) ];
      description = ''
        Left and right offset for shadows (in pixels).
      '';
    };

    shadowOpacity = mkOption {
      type = floatBetween "0.0" "1.01";
      default = "0.75";
      example = "0.8";
      description = ''
        Window shadows opacity (number in range 0.0 - 1.0).
      '';
    };

    shadowExclude = mkOption {
      type = types.listOf types.str;
      default = [];
      example = [
        "window_type *= 'menu'"
        "name ~= 'Firefox$'"
        "focused = 1"
      ];
      description = ''
        List of conditions of windows that should have no shadow.
        See <literal>compton(1)</literal> man page for more examples.
      '';
    };

    activeOpacity = mkOption {
      type = floatBetween "0.0" "1.01";
      default = "1.0";
      example = "0.8";
      description = ''
        Opacity of active windows (number in range 0.0 - 1.0).
      '';
    };

    inactiveOpacity = mkOption {
      type = floatBetween "0.1" "1.01";
      default = "1.0";
      example = "0.8";
      description = ''
        Opacity of inactive windows (number in range 0.1 - 1.0).
      '';
    };

    menuOpacity = mkOption {
      type = floatBetween "0.0" "1.01";
      default = "1.0";
      example = "0.8";
      description = ''
        Opacity of dropdown and popup menu (number in range 0.0 - 1.0).
      '';
    };

    wintypes = mkOption {
      type = types.attrs;
      default = { popup_menu = { opacity = cfg.menuOpacity; }; dropdown_menu = { opacity = cfg.menuOpacity; }; };
      example = {};
      description = ''
        Rules for specific window types.
      '';
    };

    opacityRules = mkOption {
      type = types.listOf types.str;
      default = [];
      example = [
        "95:class_g = 'URxvt' && !_NET_WM_STATE@:32a"
        "0:_NET_WM_STATE@:32a *= '_NET_WM_STATE_HIDDEN'"
      ];
      description = ''
        Rules that control the opacity of windows, in format PERCENT:PATTERN.
      '';
    };

    backend = mkOption {
      type = types.enum [ "glx" "xrender" "xr_glx_hybrid" ];
      default = "xrender";
      description = ''
        Backend to use: <literal>glx</literal>, <literal>xrender</literal> or <literal>xr_glx_hybrid</literal>.
      '';
    };

    vSync = mkOption {
      type = with types; either bool
        (enum [ "none" "drm" "opengl" "opengl-oml" "opengl-swc" "opengl-mswc" ]);
      default = false;
      apply = x:
        let
          res = x != "none";
          msg = "The type of services.compton.vSync has changed to bool:"
                + " interpreting ${x} as ${boolToString res}";
        in
          if isBool x then x
          else warn msg res;

      description = ''
        Enable vertical synchronization. Chooses the best method
        (drm, opengl, opengl-oml, opengl-swc, opengl-mswc) automatically.
        The bool value should be used, the others are just for backwards compatibility.
      '';
    };

    refreshRate = mkOption {
      type = types.addCheck types.int (x: x >= 0);
      default = 0;
      example = 60;
      description = ''
       Screen refresh rate (0 = automatically detect).
      '';
    };

    settings = let
      configTypes = with types; oneOf [ bool int float str ];
      # types.loaOf converts lists to sets
      loaOf = t: with types; either (listOf t) (attrsOf t);
    in mkOption {
      type = loaOf (types.either configTypes (loaOf (types.either configTypes (loaOf configTypes))));
      default = {};
      description = ''
        Additional Compton configuration.
      '';
    };
  };

  config = mkIf cfg.enable {
    services.compton.settings = let
      # Hard conversion to float, literally lib.toInt but toFloat
      toFloat = str: let
        may_be_float = builtins.fromJSON str;
      in if builtins.isFloat may_be_float
        then may_be_float
        else throw "Could not convert ${str} to float.";
    in {
      # fading
      fading           = mkDefault cfg.fade;
      fade-delta       = mkDefault cfg.fadeDelta;
      fade-in-step     = mkDefault (toFloat (elemAt cfg.fadeSteps 0));
      fade-out-step    = mkDefault (toFloat (elemAt cfg.fadeSteps 1));
      fade-exclude     = mkDefault cfg.fadeExclude;

      # shadows
      shadow           = mkDefault cfg.shadow;
      shadow-offset-x  = mkDefault (elemAt cfg.shadowOffsets 0);
      shadow-offset-y  = mkDefault (elemAt cfg.shadowOffsets 1);
      shadow-opacity   = mkDefault (toFloat cfg.shadowOpacity);
      shadow-exclude   = mkDefault cfg.shadowExclude;

      # opacity
      active-opacity   = mkDefault (toFloat cfg.activeOpacity);
      inactive-opacity = mkDefault (toFloat cfg.inactiveOpacity);

      wintypes         = mkDefault cfg.wintypes;

      opacity-rule     = mkDefault cfg.opacityRules;

      # other options
      backend          = mkDefault cfg.backend;
      vsync            = mkDefault cfg.vSync;
      refresh-rate     = mkDefault cfg.refreshRate;
    };

    systemd.user.services.compton = {
      description = "Compton composite manager";
      wantedBy = [ "graphical-session.target" ];
      partOf = [ "graphical-session.target" ];

      # Temporarily fixes corrupt colours with Mesa 18
      environment = mkIf (cfg.backend == "glx") {
        allow_rgb10_configs = "false";
      };

      serviceConfig = {
        ExecStart = "${pkgs.compton}/bin/compton --config ${configFile}";
        RestartSec = 3;
        Restart = "always";
      };
    };

    environment.systemPackages = [ pkgs.compton ];
  };

}
