/*
THIS FILE ISN'T LICENSED UNDER THE WTFPL LICENSE.

This file is copied from Home-manager's GitHub and was modified to suit my
(Gustavo "Guz" L. de Mello) personal needs. The original file can be found
here: https://github.com/nix-community/home-manager/blob/master/modules/programs/firefox.nix

Said file is licensed under the MIT License, which a copy is written below:

MIT License

Copyright (c) 2017-2023 Home Manager contributors
Copyright (c) 2023-present Gustavo "Guz" L. de Mello

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
{
  config,
  lib,
  pkgs,
  ...
}:
with lib; let
  cfg = config.librewolf;

  jsonFormat = pkgs.formats.json {};

  firefoxConfigPath = ".librewolf";
  profilesPath = firefoxConfigPath;

  # The extensions path shared by all profiles; will not be supported
  # by future Firefox versions.
  extensionPath = "extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";

  profiles =
    flip mapAttrs' cfg.profiles
    (_: profile:
      nameValuePair "Profile${toString profile.id}" {
        Name = profile.name;
        Path = profile.path;
        IsRelative = 1;
        Default =
          if profile.isDefault
          then 1
          else 0;
      })
    // {
      General = {StartWithLastProfile = 1;};
    };

  profilesIni = generators.toINI {} profiles;

  userPrefValue = pref:
    builtins.toJSON (
      if isBool pref || isInt pref || isString pref
      then pref
      else builtins.toJSON pref
    );

  mkUserJs = prefs: extraPrefs: bookmarks: let
    prefs' =
      lib.optionalAttrs ([] != bookmarks)
      {
        "browser.bookmarks.file" = toString (firefoxBookmarksFile bookmarks);
        "browser.places.importBookmarksHTML" = true;
      }
      // prefs;
  in ''
    // Generated by Home Manager.

    ${concatStrings (mapAttrsToList (name: value: ''
        user_pref("${name}", ${userPrefValue value});
      '')
      prefs')}

    ${extraPrefs}
  '';

  mkContainersJson = containers: let
    containerToIdentity = _: container: {
      userContextId = container.id;
      name = container.name;
      icon = container.icon;
      color = container.color;
      public = true;
    };
  in ''
    ${builtins.toJSON {
      version = 4;
      lastUserContextId =
        elemAt (mapAttrsToList (_: container: container.id) containers) 0;
      identities = mapAttrsToList containerToIdentity containers;
    }}
  '';

  firefoxBookmarksFile = bookmarks: let
    indent = level:
      lib.concatStringsSep "" (map (lib.const "  ") (lib.range 1 level));

    bookmarkToHTML = indentLevel: bookmark: ''
      ${indent indentLevel}<DT><A HREF="${
        escapeXML bookmark.url
      }" ADD_DATE="1" LAST_MODIFIED="1"${
        lib.optionalString (bookmark.keyword != null)
        " SHORTCUTURL=\"${escapeXML bookmark.keyword}\""
      }${
        lib.optionalString (bookmark.tags != [])
        " TAGS=\"${escapeXML (concatStringsSep "," bookmark.tags)}\""
      }>${escapeXML bookmark.name}</A>'';

    directoryToHTML = indentLevel: directory: ''
      ${indent indentLevel}<DT>${
        if directory.toolbar
        then ''
          <H3 ADD_DATE="1" LAST_MODIFIED="1" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar''
        else ''<H3 ADD_DATE="1" LAST_MODIFIED="1">${escapeXML directory.name}''
      }</H3>
      ${indent indentLevel}<DL><p>
      ${allItemsToHTML (indentLevel + 1) directory.bookmarks}
      ${indent indentLevel}</DL><p>'';

    itemToHTMLOrRecurse = indentLevel: item:
      if item ? "url"
      then bookmarkToHTML indentLevel item
      else directoryToHTML indentLevel item;

    allItemsToHTML = indentLevel: bookmarks:
      lib.concatStringsSep "\n"
      (map (itemToHTMLOrRecurse indentLevel) bookmarks);

    bookmarkEntries = allItemsToHTML 1 bookmarks;
  in
    pkgs.writeText "firefox-bookmarks.html" ''
      <!DOCTYPE NETSCAPE-Bookmark-file-1>
      <!-- This is an automatically generated file.
        It will be read and overwritten.
        DO NOT EDIT! -->
      <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
      <TITLE>Bookmarks</TITLE>
      <H1>Bookmarks Menu</H1>
      <DL><p>
      ${bookmarkEntries}
      </DL>
    '';

  mkNoDuplicateAssertion = entities: entityKind: (
    let
      # Return an attribute set with entity IDs as keys and a list of
      # entity names with corresponding ID as value. An ID is present in
      # the result only if more than one entity has it. The argument
      # entities is a list of AttrSet of one id/name pair.
      findDuplicateIds = entities:
        filterAttrs (_entityId: entityNames: length entityNames != 1)
        (zipAttrs entities);

      duplicates = findDuplicateIds (mapAttrsToList
        (entityName: entity: {"${toString entity.id}" = entityName;})
        entities);

      mkMsg = entityId: entityNames:
        "  - ID ${entityId} is used by " + concatStringsSep ", " entityNames;
    in {
      assertion = duplicates == {};
      message =
        ''
          Must not have a Firefox ${entityKind} with an existing ID but
        ''
        + concatStringsSep "\n" (mapAttrsToList mkMsg duplicates);
    }
  );
in {
  imports = [];
  options.librewolf = with lib;
  with lib.types; {
    enable = mkEnableOption "Enable module";
    overrides = mkOption {
      default = {};
      type = attrsOf (either bool (either int str));
    };
    profiles = mkOption {
      type = attrsOf (submodule ({
        config,
        name,
        ...
      }: {
        options = {
          name = mkOption {
            type = str;
            default = name;
          };
          id = mkOption {
            type = ints.unsigned;
            default = 0;
          };
          settings = mkOption {
            type = attrsOf (jsonFormat.type
              // {
                description = "";
              });
            default = {};
          };
          extraConfig = mkOption {
            type = lines;
            default = "";
          };
          userChrome = mkOption {
            type = lines;
            default = "";
          };
          userContent = mkOption {
            type = lines;
            default = "";
          };
          bookmarks =
            mkOption
            {
              type = let
                bookmarkSubmodule =
                  submodule
                  ({
                    config,
                    name,
                    ...
                  }: {
                    options = {
                      name = mkOption {
                        type = str;
                        default = name;
                      };
                      tags = mkOption {
                        type = listOf str;
                        default = [];
                      };
                      keyword = mkOption {
                        type = nullOr str;
                        default = null;
                      };
                      url = mkOption {
                        type = str;
                      };
                    };
                  });
                bookmarkType = addCheck bookmarkSubmodule (x: x ? "url");

                directoryType = submodule ({
                  config,
                  name,
                  ...
                }: {
                  options = {
                    name = mkOption {
                      type = str;
                      default = name;
                    };
                    bookmarks = mkOption {
                      type = listOf nodeType;
                      default = [];
                    };
                    toolbar = mkOption {
                      type = bool;
                      default = false;
                    };
                  };
                });

                nodeType = either bookmarkType directoryType;
              in
                with types;
                  coercedTo (attrsOf nodeType) attrValues (listOf nodeType);
              default = [];
            };
          path = mkOption {
            type = str;
            default = name;
          };
          isDefault = mkOption {
            type = bool;
            default = config.id == 0;
          };
          # For some reason Librewolf isn't working with the generated file, so
          # a static one is passed as a temp fix.
          search.file = mkOption {
            type = path;
            default = ./search.json.mozlz4;
          };
          /*
          search = {
            force = mkOption {
              type = bool;
              default = false;
            };
            default = mkOption {
              type = nullOr str;
              default = null;
            };
            privateDefault = mkOption {
              type = nullOr str;
              default = null;
            };
            order = mkOption {
              type = uniq (listOf str);
              default = [ ];
            };
            engines = mkOption {
              type = attrsOf (attrsOf jsonFormat.type);
              default = { };
            };
          };
          */
          containers = mkOption {
            type =
              attrsOf
              (submodule ({name, ...}: {
                options = {
                  name = mkOption {
                    type = str;
                    default = name;
                  };
                  id = mkOption {
                    type = ints.unsigned;
                    default = 0;
                  };
                  color = mkOption {
                    type = enum [
                      "blue"
                      "turquoise"
                      "green"
                      "yellow"
                      "orange"
                      "red"
                      "pink"
                      "purple"
                      "toolbar"
                    ];
                    default = "pink";
                  };
                  icon = mkOption {
                    type = enum [
                      "briefcase"
                      "cart"
                      "circle"
                      "dollar"
                      "fence"
                      "fingerprint"
                      "gift"
                      "vacation"
                      "food"
                      "fruit"
                      "pet"
                      "tree"
                      "chill"
                    ];
                    default = "fruit";
                  };
                };
              }));
            default = {};
          };
          extensions = mkOption {
            type = listOf package;
            default = [];
          };
        };
      }));
      default = {};
    };
  };
  config = lib.mkIf cfg.enable {
    programs.librewolf.enable = true;
    programs.librewolf.settings = cfg.overrides;

    home.file = mkMerge ([
        {
          "${firefoxConfigPath}/profiles.ini" = mkIf (cfg.profiles != {}) {
            text = profilesIni;
            force = true;
          };
        }
      ]
      ++ flip mapAttrsToList cfg.profiles (_: profile: {
        "${profilesPath}/${profile.path}/.keep".text = "";

        "${profilesPath}/${profile.path}/chrome/userChrome.css" =
          mkIf (profile.userChrome != "") {text = profile.userChrome;};

        "${profilesPath}/${profile.path}/chrome/userContent.css" =
          mkIf (profile.userContent != "") {text = profile.userContent;};

        "${profilesPath}/${profile.path}/user.js" =
          mkIf
          (profile.settings
            != {}
            || profile.extraConfig != ""
            || profile.bookmarks != [])
          {
            text =
              mkUserJs profile.settings profile.extraConfig profile.bookmarks;
          };

        "${profilesPath}/${profile.path}/containers.json" = mkIf (profile.containers != {}) {
          text = mkContainersJson profile.containers;
        };

        "${profilesPath}/${profile.path}/search.json.mozlz4" = {
          force = true;
          source = ./search.json.mozlz4; # Use static file, because for some reason this isn't working with Librewolf.
        };

        "${profilesPath}/${profile.path}/extensions" = mkIf (profile.extensions != []) {
          source = let
            extensionsEnvPkg = pkgs.buildEnv {
              name = "hm-firefox-extensions";
              paths = profile.extensions;
            };
          in "${extensionsEnvPkg}/share/mozilla/${extensionPath}";
          recursive = true;
          force = true;
        };
      }));
  };
}
