-- David Lettier (C) 2016. http://www.lettier.com/

-- | Produces GIFs using FFmpeg and ImageMagick.
-- The main function is 'gif'.
module Gifcurry (
      gif
    , GifParams(..)
    , defaultGifParams
    , gifParamsValid
  ) where

import System.Environment
import System.Process
import System.IO.Temp
import System.Directory
import System.Exit
import Data.List
import Text.Printf
import Control.Exception
import Control.Monad

-- | The data type record required by 'gif'.
data GifParams = GifParams {
      inputFile :: String
    , outputFile :: String
    , startTime :: Float
    , durationTime :: Float
    , widthSize :: Int
    , qualityPercent :: Float
    , topText :: String
    , bottomText :: String
  } deriving (Show, Read)

-- | Specifies default parameters for 'startTime', 'durationTime', 'widthSize', and 'qualityPercent'.
defaultGifParams = GifParams {
      inputFile = ""
    , outputFile = ""
    , startTime = 0.0
    , durationTime = 1.0
    , widthSize = 500
    , qualityPercent = 100.0
    , topText = ""
    , bottomText = ""
  }

-- | Inputs 'GifParams' and outputs either an IO IOError or IO String.
--
-- @
--    import qualified Gifcurry (gif, GifParams(..), defaultGifParams, gifParamsValid)
--    main :: IO ()
--    main = do
--      let params = Gifcurry.defaultGifParams { Gifcurry.inputFile = ".\/in.mov", Gifcurry.outputFile = ".\/out.gif" }
--      valid <- Gifcurry.gifParamsValid params
--      if valid
--        then do
--          result <- Gifcurry.gif params
--          print result
--        else return ()
-- @
gif :: GifParams -> IO (Either IOError String)
gif gifParams =
  withTempDirectory "." "frames" $ \tmpdir -> do
    printGifParams gifParams tmpdir
    validParams <- gifParamsValid gifParams
    if validParams
      then do
        result <- tryFfmpeg gifParams tmpdir
        result' <- case result of
          Left  err -> return False
          Right val -> return True
        if result'
          then do
            putStrLn $ "Writing your GIF to... " ++ outputFile gifParams
            result <- tryConvert gifParams tmpdir
            result' <- case result of
               Left  err -> return False
               Right val -> return True
            if result'
              then putStrLn "Done."
              else putStrLn "[Error] Something when wrong with ImageMagick."
            return result
          else do
            putStrLn "[Error] Something went wrong with FFmpeg."
            return result
      else return $ Left (userError "[Error] Invalid params.")

-- | Outputs True or False if a GifParams record parameters are valid.
-- Looks at 'inputFile', 'outputFile', 'startTime', 'durationTime', 'widthSize', and 'qualityPercent'.
gifParamsValid :: GifParams -> IO Bool
gifParamsValid GifParams {
      inputFile = ipf
    , outputFile = opf
    , startTime = st
    , durationTime = dt
    , widthSize = ws
    , qualityPercent = qp
    , topText = tt
    , bottomText = bt
  } = do
    inputFileExists <- case length ipf of
      0 -> return False
      _ -> doesFileExist ipf
    unless inputFileExists $ putStrLn "\n[Error] Input video file does not exist."
    let outputFileValid = length opf > 5
    unless outputFileValid $ putStrLn "\n[Error] Output video file blank."
    let valid = inputFileExists && outputFileValid && (st >= 0.0) && (dt >= 0.0) && (ws > 0) && (qp > 0.0)
    unless valid $ putStrLn "\n[Error] Invalid params."
    return valid

printGifParams :: GifParams -> String -> IO ()
printGifParams
  GifParams {
      inputFile = ipf
    , outputFile = opf
    , startTime = st
    , durationTime = dt
    , widthSize = ws
    , qualityPercent = qp
    , topText = tt
    , bottomText = bt
  }
  tmpdir = mapM_ putStrLn [
        "\nInput file: " ++ ipf
      , "Output file: " ++ opf
      , "Start second: " ++ printf "%.3f" st
      , "Duration: " ++ printf "%.3f" dt ++ " seconds"
      , "GIF width: " ++ show ws ++ "px"
      , "Quality: " ++ show (qualityPercentClamp qp) ++ "%"
      , "Top text: " ++ tt
      , "Bottom text: " ++ bt
      , "\nWriting temporary frames to... " ++ tmpdir
    ]

tryFfmpeg :: GifParams -> String -> IO (Either IOError String)
tryFfmpeg
  GifParams {
      inputFile = ipf
    , startTime = st
    , durationTime = dt
    , widthSize = ws
  }
  tmpdir = try(
    readProcess "ffmpeg" [
        "-nostats"
      , "-loglevel"
      , "panic"
      , "-an"
      , "-ss"
      , sts
      , "-i"
      , ipf
      , "-t"
      , dts
      , "-r"
      , "15"
      , "-q:v"
      , "2"
      , "-vf"
      , "scale=" ++ wss ++ ":-1"
      , "-f"
      , "image2"
      , tmpdir ++ "/%010d.png"
    ] ""
  ) :: IO (Either IOError String)
  where sts = printf "%.3f" st
        dts = printf "%.3f" dt
        wss = show ws

tryConvert :: GifParams -> String -> IO (Either IOError String)
tryConvert
  GifParams {
      outputFile = opf
    , widthSize = ws
    , qualityPercent = qp
    , topText = tt
    , bottomText = bt
  }
  tmpdir = try(
    readProcess "convert" (
      [
          "-quiet"
        , "-delay"
        , "6"
        , "-colors"
        , show $ ncolors qp
        , "-coalesce"
        , "-layers"
        , "OptimizeTransparency"
        , "-layers"
        , "RemoveDups"
        , tmpdir ++ "/*.png"
        , "-dither"
        , "FloydSteinberg"
        , "-loop"
        , "0"
      ] ++ annotate ws tt "north" ++ annotate ws bt "south" ++ [opf]
    ) ""
  )

qualityPercentClamp :: Float -> Float
qualityPercentClamp qp
  | qp > 100.0   = 100.0
  | qp < 0.0     = 2.0
  | otherwise    = qp

ncolors :: Float -> Int
ncolors qp
  | qpc < 0.0    = 1
  | qpc >= 100.0 = 256
  | otherwise  = truncate (qpc / 100.0 * 256.0)
  where qpc = qualityPercentClamp qp

annotate :: Int -> String -> String -> [String]
annotate widthSize text topBottom = [
      "-gravity"
    , topBottom
    , "-stroke"
    , "#000C"
    , "-strokewidth"
    , "10"
    , "-pointsize"
    , ps
    , "-annotate"
    , "+0+10"
    , text
    , "-stroke"
    , "none"
    , "-fill"
    , "white"
    , "-pointsize"
    , ps
    , "-annotate"
    , "+0+10"
    , text
  ]
  where ps = show $ pointSize widthSize text

pointSize :: Int -> String -> Int
pointsize _ "" = 0
pointSize widthSize text
  | widthSize <= 0  = 0
  | otherwise       = truncate ((wsf * 0.4) / l * (72.0 / 34.0))
  where wsf = fromIntegral widthSize
        l   = fromIntegral (length text)