{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs            #-}

module System.Console.GetOpt.Generics.Modifier (
  Modifier(..),
  deriveShortOptions,
  mkShortOptions,
  mkLongOptions,
 ) where

import           Data.List
import           Data.Maybe
import           Generics.SOP

import           System.Console.GetOpt.Generics.Internal

-- | 'Modifier's can be used to customize the command line parser.
data Modifier
  = AddShortOption String Char
    -- ^ @AddShortOption fieldName c@ adds the 'Char' @c@ as a short option for
    --   the field addressed by @fieldName@.
  | RenameOption String String
    -- ^ @RenameOption fieldName customName@ renames the option generated
    --   through the @fieldName@ by @customName@.
  deriving (Show, Eq, Ord)

-- | Derives 'AddShortOption's for all fields of the datatype that start with a
--   unique character.
deriveShortOptions :: (HasDatatypeInfo a, SingI (Code a)) =>
  Proxy a -> [Modifier]
deriveShortOptions proxy =
  mkShortModifiers (flags proxy)

flags :: (SingI (Code a), HasDatatypeInfo a) =>
  Proxy a -> [String]
flags proxy = case normalizedDatatypeInfo proxy of
    ADT _ _ ci -> fromNPConstructorInfo ci
    Newtype _ _ ci -> fromConstructorInfo ci
  where
    fromNPConstructorInfo :: NP ConstructorInfo xs -> [String]
    fromNPConstructorInfo Nil = []
    fromNPConstructorInfo (a :* r) =
      fromConstructorInfo a ++ fromNPConstructorInfo r

    fromConstructorInfo :: ConstructorInfo x -> [String]
    fromConstructorInfo (Constructor _) = []
    fromConstructorInfo (Infix _ _ _) = []
    fromConstructorInfo (Record _ fields) =
      fromFields fields

    fromFields :: NP FieldInfo xs -> [String]
    fromFields (FieldInfo name :* r) = name : fromFields r
    fromFields Nil = []

mkShortModifiers :: [String] -> [Modifier]
mkShortModifiers fields =
    mapMaybe inner fields
  where
    inner :: String -> Maybe Modifier
    inner field@(short : _) =
      case filter ([short] `isPrefixOf`) fields of
        [_] -> Just $ AddShortOption field short
        _ -> Nothing
    inner [] = Nothing

mkShortOptions :: [Modifier] -> String -> [Char]
mkShortOptions modifiers option =
    mapMaybe inner modifiers
  where
    inner :: Modifier -> Maybe Char
    inner (AddShortOption modifierOption short)
      | matchesField modifierOption option
        = Just short
      | otherwise
        = Nothing
    inner _ = Nothing

mkLongOptions :: [Modifier] -> String -> [String]
mkLongOptions modifiers option =
    inner (reverse modifiers)
  where
    inner (RenameOption renameOption newName : _)
      | renameOption `matchesField` option = [newName]
    inner [] = [option]
    inner (_ : r) = inner r

matchesField :: String -> String -> Bool
matchesField modifierOption option =
  modifierOption == option || slugify modifierOption == option