    module System.Console.Haskeline.Key(Key(..),
                Modifier(..),
                BaseKey(..),
                noModifier,
                simpleKey,
                simpleChar,
                metaChar,
                ctrlChar,
                metaKey,
                ctrlKey,
                parseKey
                ) where

    import Data.Char
    import Control.Monad
    import Data.Maybe
    import Data.Bits

    data Key = Key Modifier BaseKey
                deriving (Show,Eq,Ord)

    data Modifier = Modifier {hasControl, hasMeta, hasShift :: Bool}
                deriving (Eq,Ord)

    instance Show Modifier where
        show m = show $ catMaybes [maybeUse hasControl "ctrl"
                            , maybeUse hasMeta "meta"
                            , maybeUse hasShift "shift"
                            ]
            where
                maybeUse f str = if f m then Just str else Nothing

    noModifier :: Modifier
    noModifier = Modifier False False False

    data BaseKey = KeyChar Char
                 | FunKey Int
                 | LeftKey | RightKey | DownKey | UpKey
                 -- TODO: is KillLine really a key?
                 | KillLine | Home | End | PageDown | PageUp
                 | Backspace | Delete
                deriving (Show,Eq,Ord)

    simpleKey :: BaseKey -> Key
    simpleKey = Key noModifier

    metaKey :: Key -> Key
    metaKey (Key m bc) = Key m {hasMeta = True} bc

    ctrlKey :: Key -> Key
    ctrlKey (Key m bc) = Key m {hasControl = True} bc

    simpleChar, metaChar, ctrlChar :: Char -> Key
    simpleChar = simpleKey . KeyChar
    metaChar = metaKey . simpleChar

    ctrlChar = simpleChar . setControlBits

    setControlBits :: Char -> Char
    setControlBits '?' = toEnum 127
    setControlBits c = toEnum $ fromEnum c .&. complement (bit 5 .|. bit 6)

    specialKeys :: [(String,BaseKey)]
    specialKeys = [("left",LeftKey)
                  ,("right",RightKey)
                  ,("down",DownKey)
                  ,("up",UpKey)
                  ,("killline",KillLine)
                  ,("home",Home)
                  ,("end",End)
                  ,("pagedown",PageDown)
                  ,("pageup",PageUp)
                  ,("backspace",Backspace)
                  ,("delete",Delete)
                  ,("return",KeyChar '\n')
                  ,("enter",KeyChar '\n')
                  ,("tab",KeyChar '\t')
                  ,("esc",KeyChar '\ESC')
                  ,("escape",KeyChar '\ESC')
                  ]

    parseModifiers :: [String] -> BaseKey -> Key
    parseModifiers strs = Key mods
        where mods = foldl1 (.) (map parseModifier strs) noModifier

    parseModifier :: String -> (Modifier -> Modifier)
    parseModifier str m = case map toLower str of
        "ctrl" -> m {hasControl = True}
        "control" -> m {hasControl = True}
        "meta" -> m {hasMeta = True}
        "shift" -> m {hasShift = True}
        _ -> m

    breakAtDashes :: String -> [String]
    breakAtDashes "" = []
    breakAtDashes str = case break (=='-') str of
        (xs,'-':rest) -> xs : breakAtDashes rest
        (xs,_) -> [xs]

    parseKey :: String -> Maybe Key
    parseKey str = fmap canonicalizeKey $
        case reverse (breakAtDashes str) of
            [ks] -> liftM simpleKey (parseBaseKey ks)
            ks:ms -> liftM (parseModifiers ms) (parseBaseKey ks)
            [] -> Nothing

    parseBaseKey :: String -> Maybe BaseKey
    parseBaseKey ks = lookup (map toLower ks) specialKeys
                    `mplus` parseFunctionKey ks
                    `mplus` parseKeyChar ks
        where
            parseKeyChar [c] | isPrint c = Just (KeyChar c)
            parseKeyChar _ = Nothing

            parseFunctionKey (f:ns) | f `elem` "fF" = case reads ns of
                [(n,"")]    -> Just (FunKey n)
                _           -> Nothing
            parseFunctionKey _ = Nothing

    canonicalizeKey :: Key -> Key
    canonicalizeKey (Key m (KeyChar c))
        | hasControl m = Key m {hasControl = False}
                            (KeyChar (setControlBits c))
        | hasShift m = Key m {hasShift = False} (KeyChar (toUpper c))
    canonicalizeKey k = k