content: module Reanimate.External
    ( URL,
      SHA256,
      zipArchive,
      tarball,
  
      -- * External Icon Datasets
      simpleIcon,
      simpleIconColor,
      simpleIcons,
      svgLogo,
      svgLogos,
    )
  where
  
  import           Codec.Picture          (PixelRGB8 (..))
  import           Control.Monad          (unless)
  import           Crypto.Hash.SHA256     (hash)
  import           Data.Aeson             (decodeFileStrict)
  import qualified Data.ByteString        as B (readFile)
  import           Data.ByteString.Base64 (encode)
  import qualified Data.ByteString.Char8  as B8 (unpack)
  import           Data.Char              (isSpace, toLower)
  import           Data.List              (sort)
  import           Data.Map               (Map)
  import qualified Data.Map               as M
  import           Numeric                (readHex)
  import           Reanimate.Animation    (SVG)
  import           Reanimate.Constants    (screenHeight, screenWidth)
  import           Reanimate.Misc         (getReanimateCacheDirectory, withTempFile)
  import           Reanimate.Raster       (mkImage)
  import           System.Directory       (doesDirectoryExist, doesFileExist, findExecutable,
                                           getDirectoryContents)
  import           System.FilePath        (splitExtension, (<.>), (</>))
  import           System.IO.Unsafe       (unsafePerformIO)
  import           System.Process         (callProcess)
  
  -- | Resource address
  type URL = String
  
  -- | Resource hash
  type SHA256 = String
  
  fetchStaticFile :: URL -> SHA256 -> (FilePath -> FilePath -> IO ()) -> IO FilePath
  fetchStaticFile url sha256 unpack = do
    root <- getReanimateCacheDirectory
    let folder = root </> sha256
    hit <- doesDirectoryExist folder
    unless hit $
      downloadFile url $ \path -> do
        inp <- B.readFile path
        let inpSha = B8.unpack (encode (hash inp))
        if inpSha == sha256
          then do
            unpack folder path
          else
            error $
              "URL " ++ url ++ "\n"
                ++ "  Expected SHA256: "
                ++ sha256
                ++ "\n"
                ++ "  Actual SHA256:   "
                ++ inpSha
    return folder
  
  {-# NOINLINE zipArchive #-}
  
  -- | Download and unpack zip archive. The returned path is the unpacked folder.
  zipArchive :: URL -> SHA256 -> FilePath
  zipArchive url sha256 = unsafePerformIO $
    fetchStaticFile url sha256 $ \folder zipfile ->
      callProcess "unzip" ["-qq", "-d", folder, zipfile]
  
  {-# NOINLINE tarball #-}
  
  -- | Download and unpack tarball. The returned path is the unpacked folder.
  tarball :: URL -> SHA256 -> FilePath
  tarball url sha256 = unsafePerformIO $
    fetchStaticFile url sha256 $ \folder tarfile ->
      callProcess "tar" ["--overwrite", "--one-top-level=" ++ folder, "-xzf", tarfile]
  
  downloadFile :: URL -> (FilePath -> IO a) -> IO a
  downloadFile url action = do
    mbCurl <- findExecutable "curl"
    mbWget <- findExecutable "wget"
    case (mbCurl, mbWget) of
      (Just curl, _)     -> downloadFileCurl curl url action
      (_, Just wget)     -> downloadFileWget wget url action
      (Nothing, Nothing) -> error "curl/wget required to download files"
  
  downloadFileCurl :: FilePath -> URL -> (FilePath -> IO a) -> IO a
  downloadFileCurl curl url action = withTempFile "dl" $ \path -> do
    callProcess
      curl
      [ url,
        "--location",
        "--output",
        path,
        "--silent",
        "--show-error",
        "--max-filesize",
        "10M",
        "--max-time",
        "60"
      ]
    action path
  
  downloadFileWget :: FilePath -> URL -> (FilePath -> IO a) -> IO a
  downloadFileWget wget url action = withTempFile "dl" $ \path -> do
    callProcess
      wget
      [ url,
        "--output-document=" ++ path,
        "--quiet"
      ]
    action path
  
  
  
  -------------------------------------------------------------------------------
  -- SimpleIcons
  
  simpleIconsFolder :: FilePath
  simpleIconsFolder =
    tarball
      "https://github.com/simple-icons/simple-icons/archive/3.11.0.tar.gz"
      "NXa8TrHHuQofrPbqTf0pBGt1GDRfuQ4IcQ7kNEk9OcQ="
      </> "simple-icons-3.11.0"
  
  {-# NOINLINE simpleIconPath #-}
  simpleIconPath :: String -> FilePath
  simpleIconPath key = unsafePerformIO $ do
    let path = simpleIconsFolder </> "icons" </> key <.> "svg"
    hit <- doesFileExist path
    if hit
      then pure path
      else error $ "Key not found in simple-icons dataset: " ++ show key
  
  -- | Icons from <https://simpleicons.org/>. Version 3.11.0. License: CC0
  --
  -- @
  -- let icon = "cplusplus" in `Reanimate.mkGroup`
  -- [ `Reanimate.mkBackgroundPixel` (`Codec.Picture.Types.promotePixel` $ `simpleIconColor` icon)
  -- , `Reanimate.withFillOpacity` 1 $ `simpleIcon` icon ]
  -- @
  --
  --   <<docs/gifs/doc_simpleIcon.gif>>
  simpleIcon :: String -> SVG
  simpleIcon = mkImage screenWidth screenHeight . simpleIconPath
  
  -- | Simple Icons svgs do not contain color. Instead, each icon has an associated color value.
  simpleIconColor :: String -> PixelRGB8
  simpleIconColor key =
    case M.lookup key simpleIconColors of
      Nothing    -> error $ "Key not found in simple-icons dataset: " ++ show key
      Just pixel -> pixel
  
  simpleIconColors :: Map String PixelRGB8
  simpleIconColors = unsafePerformIO $ do
    let path = simpleIconsFolder </> "_data" </> "simple-icons.json"
    mbRet <- decodeFileStrict path
    let parsed = do
          m <- mbRet
          icons <- M.lookup "icons" m
          pure $
            M.fromList
              [ (fromTitle title, parseHex hex) | icon <- icons, Just title <- [M.lookup "title" icon], Just hex <- [M.lookup "hex" icon]
              ]
    case parsed of
      Nothing -> error "Invalid json in simpleIcons"
      Just v  -> pure v
    where
      fromTitle :: String -> String
      fromTitle = replaceChars . map toLower
  
      replaceChars :: String -> String
      replaceChars ('.' : x : xs) = "dot-" ++ replaceChars (x : xs)
      replaceChars "." = "dot"
      replaceChars (x : '.' : []) = replaceChars (x : "-dot")
      replaceChars (x : '.' : xs) = replaceChars (x : "-dot-" ++ xs)
      replaceChars (x : xs)
        | isSpace x || x `elem` "!:'’" = replaceChars xs
      replaceChars ('&' : xs) = "-and-" ++ replaceChars xs
      replaceChars ('+' : xs) = "plus" ++ replaceChars xs
      replaceChars (x : xs)
        | x `elem` "àáâãä" = 'a' : replaceChars xs
        | x `elem` "ìíîï" = 'i' : replaceChars xs
        | x `elem` "èéêë" = 'e' : replaceChars xs
        | x `elem` "šś" = 's' : replaceChars xs
      replaceChars (x : xs) = x : replaceChars xs
      replaceChars [] = []
      parseHex :: String -> PixelRGB8
      parseHex hex = PixelRGB8 (p 0) (p 2) (p 4)
        where
          p offset = case readHex (take 2 $ drop offset hex) of
            [(num, "")] -> num
            _           -> error $ "Invalid hex: " ++ (take 2 $ drop offset hex)
  
  {-# NOINLINE simpleIcons #-}
  -- | Complete list of all Simple Icons.
  simpleIcons :: [String]
  simpleIcons = unsafePerformIO $ do
    let folder = simpleIconsFolder </> "icons"
    files <- getDirectoryContents folder
    return $
      sort
        [key | file <- files, let (key, ext) = splitExtension file, ext == ".svg"]
  
  
  svgLogosFolder :: FilePath
  svgLogosFolder = tarball
      "https://github.com/gilbarbara/logos/archive/2018.01.tar.gz"
      "kRRA0cF6sVOyqtfVW8EMew4OB4WJcY81DEGS3FLEY8Y="
      </> "logos-2018.01" </> "logos"
  
  {-# NOINLINE svgLogoPath #-}
  svgLogoPath :: String -> FilePath
  svgLogoPath key = unsafePerformIO $ do
    let path = svgLogosFolder </> key <.> "svg"
    hit <- doesFileExist path
    if hit
      then pure path
      else error $ "Key not found in svg logos dataset: " ++ show key
  
  -- | Icons from <https://svgporn.com/>. Version 2018.01. License: CC0
  --
  -- @
  -- `svgLogo` "cassandra"
  -- @
  --
  --   <<docs/gifs/doc_svgLogo2.gif>>
  svgLogo :: String -> SVG
  svgLogo = mkImage screenWidth screenHeight . svgLogoPath
  
  {-# NOINLINE svgLogos #-}
  -- | Complete list of all SVG Icons.
  svgLogos :: [String]
  svgLogos = unsafePerformIO $ do
    files <- getDirectoryContents svgLogosFolder
    return $
      sort
        [key | file <- files, let (key, ext) = splitExtension file, ext == ".svg"]
  