{
  config,
  lib,
  pkgs,
  ...
}:
let
  inherit (lib)
    mkIf
    mkOption
    types
    ;

  cfg = config.programs.w3m;
in
{
  meta.maintainers = with lib.hm.maintainers; [ oneorseveralcats ];

  options.programs.w3m = {
    enable = lib.mkEnableOption "the w3m terminal web browser";

    package = lib.mkPackageOption pkgs "w3m" { nullable = true; };

    finalPackage = mkOption {
      type = types.package;
      readOnly = true;
      description = ''
        Final W3m package that respects homePage, xdg.enable,
        and w3mImg2Sixel.
      '';
    };

    homePage = mkOption {
      type = types.str;
      default = "https://duckduckgo.com";
      example = "\${config.xdg.configHome}/w3m/bookmark.html";
      description = "Page w3m opens to if a url isn't provided.";
    };

    w3mImg2Sixel = mkOption {
      type = with types; nullOr str;
      default = "img2sixel";
      example = "img2sixel -d atkinson";
      description = ''
        The executable and arguments that w3m should execute when using libsixel
        as the image backend.
      '';
    };

    bindings = mkOption {
      type = with types; attrsOf str;
      default = { };
      example = {
        "gg" = "BEGIN";
        "C-a" = "LINE_BEGIN";
        "O" =
          ''COMMAND "SET_OPTION dictprompt='GOTO: '; SET_OPTION dictcommand=file:/cgi-bin/opener.cgi ; DICT_WORD"'';
        "\\\"" = "REG_MARK";
        "\";\"" = "MARK_WORD";
        "\\^" = "LINE_BEGIN";
        "M-TAB" = "PREV_LINK";
        "M-C-j" = "SAVE_LINK";
        "DEL" = "CLOSE_TAB";
        "SPC" = "NEXT_PAGE";
        "UP" = "MOVE_UP";
      };
      description = ''
        Keybindings for w3m.

        See <https://git.sr.ht/~rkta/w3m/tree/master/item/doc/README.keymap> for
        documentation.
      '';
    };

    bookmarks = {
      title = mkOption {
        type = types.str;
        default = "Bookmarks";
        description = ''
          Title of the bookmarks page.
        '';
      };

      marks = mkOption {
        type =
          let
            bookmarkType = types.submodule {
              options = {
                name = mkOption {
                  type = types.str;
                  description = ''
                    Display name of bookmark.
                  '';
                };

                url = mkOption {
                  type = types.str;
                  description = ''
                    Destination address of bookmark.
                  '';
                };
              };
            };
          in
          with types;
          attrsOf (listOf bookmarkType);
        default = { };
        example = {
          nix = [
            {
              name = "nixos manual";
              url = "https://nixos.org/manual/nixos/stable/";
            }
            {
              name = "home-manager manual";
              url = "https://nix-community.github.io/home-manager/";
            }
          ];
          archlinux = [
            {
              name = "aur";
              url = "https://aur.archlinux.org/";
            }
            {
              name = "archwiki";
              url = "https://wiki.archlinux.org/title/Main_page";
            }
          ];
        };
        description = ''
          Bookmark file for w3m.
        '';
      };
    };

    cgiBin = mkOption {
      type =
        let
          fileType = types.submodule {
            options = {
              source = mkOption {
                type = with types; nullOr path;
                default = null;
                description = ''
                  Path to script file.
                '';
              };
              text = mkOption {
                type = with types; nullOr lines;
                default = null;
                description = ''
                  Inline content of script file.
                '';
              };
            };
          };
        in
        types.attrsOf fileType;
      default = { };
      example = {
        "search.cgi".text = ''
          #!/usr/bin/env sh

          PREFIX=$(echo "$QUERY_STRING" | cut -d ':' -f1)
          INPUT=$(echo "$QUERY_STRING" | cut -d ':' -f2-)

          case $PREFIX in
            aw) echo "W3m-control: GOTO https://wiki.archlinux.org/index.php?search=$INPUT";;
            ddg) echo "W3m-control: GOTO https://lite.duckduckgo.com/lite/?q=$INPUT";;
          esac

          echo "W3m-control: DELETE_PREVBUF"
        '';
      };
      description = ''
        Scripts located in w3m's cgi-bin directory. For security reasons, w3m can
        only read scripts from here and {file}`''${pkgs.w3m}/libexec/w3m/cgi-bin/`
        (referenceable as $LIB in w3m). The cgi-bin scripts can be written in any
        language and have access to the query provided to them through the
        QUERY_STRING environment variable. A cgi-bin script can send commands
        back to w3m via stdout with the form "W3m-control: <command>".

        See <https://git.sr.ht/~rkta/w3m/tree/master/item/doc/MANUAL.html> for
        more information.

        As of w3m v0.5.5, the option `cgi_bin` isn't defined by default. If you
        want to use any cgi-bin scripts in w3m then set
        `programs.w3m.settings.cgi_bin`.
      '';
    };

    settings = mkOption {
      type = with types; attrsOf (either str int);
      default = { };
      example = {
        cgi_bin = "\${config.xdg.configHome}/w3m/cgi-bin";
        urimethodmap = "\${config.xdg.configHome}/w3m/urimethodmap";
        siteconf_file = "\${config.xdg.configHome}/w3m/siteconf";

        tabstop = 4;
        extbrowser = "firefox";
      };
      description = ''
        Settings for w3m typically set on the OPTIONS page. The best way to
        configure them is setting them in w3m then nixifying the w3m `config`
        file located at either {file}`~/.w3m/config` or
        {file}`~/$XDG_CONFIG_HOME/w3m/config`.
      '';
    };

    siteconf = mkOption {
      type =
        let
          entryType = types.submodule {
            options = {
              url = mkOption {
                type = types.str;
                description = ''
                  The url that the preferences should apply to. Can be of the
                  form `<url>`, `m!<regex>!`, `m@<regex>@`, or `/<regex>/` with
                  optional trailing "i" for case insensitive and "exact" for exact
                  matches.
                '';
              };
              preferences = mkOption {
                type = with types; listOf str;
                description = ''
                  The preferences that w3m can apply to the matched url. Options
                  are: `substitute_url "<destination-url>"`,
                  `url_charset <charset>`, `no_referer_from on|off`,
                  `no_referer_to on|off`, `user_agent "string"`.
                '';
              };
            };
          };
        in
        types.listOf entryType;
      default = [ ];
      example = [
        {
          url = "m!^https://duckduckgo.com/!i";
          preferences = [
            ''substitute_url "https://lite.duckduckgo.com"''
          ];
        }
        {
          url = "m!^https://wikipedia.org/! exact";
          preferences = [
            "url_charset utf-8"
            ''substitute_url "https://eo.wikipedia.org"''
          ];
        }
      ];
      description = ''
        Settings for w3m's siteconf. It allows you to match on a url pattern
        and do various things like url substitutions, site-specific user agent
        settings, specifying charset, and a few others.

        See <https://git.sr.ht/~rkta/w3m/tree/master/item/doc/README.siteconf>
        for documentation and examples.

        As of w3m v0.5.5, siteconf doesn't respect the W3M_DIR environment
        variable, so unless `programs.w3m.settings.siteconf_file` is set,
        `siteconf` will always be at {file}`~/.w3m/siteconf`.
      '';
    };

    urimethodmap = mkOption {
      type = with types; attrsOf str;
      default = { };
      example = {
        ddg = "file:/cgi-bin/search.cgi?%s";
        help = "file:/$LIB/w3mhelp.cgi?%s";
      };
      description = ''
        Settings for w3m's urimethodmap. It allows you to define custom uri
        schemes and map them to scripts. Scripts must be in the directory
        defined in `programs.w3m.settings.cgi_bin`.

        As of w3m v0.5.5, urimethodmap doesn't respect the W3M_DIR environment
        variable, so unless `programs.w3m.settings.urimethodmap` is set,
        `urimethodmap` will always be at {file}`~/.w3m/urimethodmap`.
      '';
    };

    extraPackages = mkOption {
      type = with types; listOf package;
      default = [ ];
      example = lib.literalExpression "[ pkgs.rdrview pkgs.libsixel ]";
      description = "Extra packages available to w3m.";
    };
  };

  config =
    let
      w3mPackage = pkgs.symlinkJoin {
        name = "w3m-wrapped";
        paths = [ cfg.package ];
        buildInputs = [ pkgs.makeWrapper ];
        postBuild = ''
          wrapProgram $out/bin/w3m \
          --set W3M_DIR "${w3mDir}" \
          --set W3M_IMG2SIXEL "${cfg.w3mImg2Sixel}" \
          --set WWW_HOME "${cfg.homePage}" \
          --suffix PATH : ${lib.makeBinPath cfg.extraPackages}
        '';
      };

      w3mDir =
        if config.xdg.enable then "${config.xdg.configHome}/w3m" else "${config.home.homeDirectory}/.w3m";

      bookmarkFile = "${w3mDir}/bookmark.html";
      configFile = "${w3mDir}/config";
      keymapFile = cfg.settings.keymap_file or "${w3mDir}/keymap";

      # these files currently don't respect the W3M_DIR environment variable so,
      # if not configured in programs.w3m.settings, they're expected to be at
      # ~/.w3m. This will likely be fixed in w3m versions after v0.5.6.
      urimethodmapFile = cfg.settings.urimethodmap or "${config.home.homeDirectory}/.w3m/urimethodmap";
      siteconfFile = cfg.settings.siteconf_file or "${config.home.homeDirectory}/.w3m/siteconf";
      cgiBinDir = cfg.settings.cgi_bin or "${config.home.homeDirectory}/.w3m/cgi-bin";

      # prepends the path of the cgiBinDir to the script name, explicitly generates
      # and replaces any text attribute with equivalent
      # "source = pkgs.writeScript ..."
      cgiScripts = lib.mapAttrs' (
        k: v:
        lib.nameValuePair "${cgiBinDir}/${k}" (
          if (v.text != null) then { source = pkgs.writeScript k v.text; } else v
        )
      ) cfg.cgiBin;

      mkConfig =
        {
          pre ? "",
          sep ? " ",
        }:
        set:
        lib.generators.toKeyValue {
          indent = pre;
          mkKeyValue = lib.generators.mkKeyValueDefault { } sep;
        } set;

      # put at the top of configuration files.
      warningHeader = ''
        # This file was generated by Home Manager and is read-only.

      '';
    in
    mkIf cfg.enable {
      assertions = [
        {
          assertion = (lib.filterAttrs (_k: v: !isNull v.text && !isNull v.source) cfg.cgiBin) == { };
          message = "Cannot specify both `.text` and `.source` options for `programs.w3m.cgiBin` scripts.";
        }
      ];

      programs.w3m.finalPackage = w3mPackage;

      home.packages = lib.mkIf (cfg.package != null) [
        w3mPackage
      ];

      home.file = {
        # generates w3m's bookmark file. the format is:
        #   <h1>title</h1>
        #   <h2>category1</h2>
        #   <ul>
        #     <li>bookmark</li>
        #     ...
        #   </ul>
        #
        #   <h2>category2</h2>
        #   ...
        "${bookmarkFile}" = mkIf (cfg.bookmarks.marks != { }) {
          source =
            let
              mkBookmarks =
                with lib;
                set:
                concatStringsSep "\n" (
                  flatten (
                    mapAttrsToList (
                      k: v:
                      [ "<h2>${k}</h2>" ]
                      ++ [ "<ul>" ]
                      ++ (map (s: "<li><a href=\"${s.url}\">${s.name}</a></li>") v)
                      ++ [ "</ul" ]
                    ) set
                  )
                );
            in
            pkgs.writeText "bookmark.html" ''
              <!-- This file was generated by Home Manager and is read-only. -->
              <!DOCTYPE html>
              <html>
              <head>
              </head>
              <body>
              <h1>${cfg.bookmarks.title}</h1>
              ${mkBookmarks cfg.bookmarks.marks}
              </body>
              </html>
            '';
        };

        # generates w3m's keybinding file. the format is:
        #   keymap <key(s)> <action(s)>
        "${keymapFile}" = mkIf (cfg.bindings != { }) {
          source = pkgs.writeText "keymap" (warningHeader + (mkConfig { pre = "keymap "; } cfg.bindings));
        };

        # generates w3m's main config file. the format is:
        #   <option> <value>
        "${configFile}" = mkIf (cfg.settings != { }) {
          source = pkgs.writeText "config" (warningHeader + (mkConfig { } cfg.settings));
        };

        # generates w3m's siteconf file. the format is:
        #   url <pattern>
        #   <option> <argument>
        #   <option> <argument>
        #   ...
        #
        #
        #   url <pattern>
        #   ...
        "${siteconfFile}" = mkIf (cfg.siteconf != [ ]) {
          source =
            let
              mkSiteConf =
                with lib;
                set: concatStringsSep "\n" (flatten (map (s: [ "url ${s.url}" ] ++ s.preferences ++ [ "\n" ]) set));
            in
            pkgs.writeText "siteconf" (warningHeader + (mkSiteConf cfg.siteconf));
        };

        # generates w3m's urimethodmap file. the format is:
        #   <uri_scheme>: <script>
        "${urimethodmapFile}" = mkIf (cfg.urimethodmap != { }) {
          source = pkgs.writeText "urimethodmap" (
            warningHeader + (mkConfig { sep = ": "; } cfg.urimethodmap)
          );
        };
      }
      // cgiScripts;
    };
}
