module XMonad.Actions.GridSelect (
    
    
    
    
    
    
    
    
    GSConfig(..),
    def,
    defaultGSConfig,
    TwoDPosition,
    buildDefaultGSConfig,
    
    gridselect,
    gridselectWindow,
    withSelectedWindow,
    bringSelected,
    goToSelected,
    gridselectWorkspace,
    gridselectWorkspace',
    spawnSelected,
    runSelectedAction,
    
    HasColorizer(defaultColorizer),
    fromClassName,
    stringColorizer,
    colorRangeFromClassName,
    
    TwoD,
    makeXEventhandler,
    shadowWithKeymap,
    
    defaultNavigation,
    substringSearch,
    navNSearch,
    
    setPos,
    move,
    moveNext, movePrev,
    select,
    cancel,
    transformSearchString,
    
    
    Rearranger,
    noRearranger,
    searchStringRearrangerGenerator,
    
    
    
    TwoDState,
    ) where
import Data.Maybe
import Data.Bits
import Data.Char
import Data.Ord (comparing)
import Control.Applicative
import Control.Monad.State
import Control.Arrow
import Data.List as L
import qualified Data.Map as M
import XMonad hiding (liftX)
import XMonad.Util.Font
import XMonad.Prompt (mkUnmanagedWindow)
import XMonad.StackSet as W
import XMonad.Layout.Decoration
import XMonad.Util.NamedWindows
import XMonad.Actions.WindowBringer (bringWindow)
import Text.Printf
import System.Random (mkStdGen, genRange, next)
import Data.Word (Word8)
data GSConfig a = GSConfig {
      gs_cellheight :: Integer,
      gs_cellwidth :: Integer,
      gs_cellpadding :: Integer,
      gs_colorizer :: a -> Bool -> X (String, String),
      gs_font :: String,
      gs_navigate :: TwoD a (Maybe a),
      gs_rearranger :: Rearranger a,
      gs_originFractX :: Double,
      gs_originFractY :: Double
}
class HasColorizer a where
    defaultColorizer :: a -> Bool -> X (String, String)
instance HasColorizer Window where
    defaultColorizer = fromClassName
instance HasColorizer String where
    defaultColorizer = stringColorizer
instance HasColorizer a where
    defaultColorizer _ isFg =
        let getColor = if isFg then focusedBorderColor else normalBorderColor
        in asks $ flip (,) "black" . getColor . config
instance HasColorizer a => Default (GSConfig a) where
    def = buildDefaultGSConfig defaultColorizer
defaultGSConfig :: HasColorizer a => GSConfig a
defaultGSConfig = def
type TwoDPosition = (Integer, Integer)
type TwoDElementMap a = [(TwoDPosition,(String,a))]
data TwoDState a = TwoDState { td_curpos :: TwoDPosition
                             , td_availSlots :: [TwoDPosition]
                             , td_elements :: [(String,a)]
                             , td_gsconfig :: GSConfig a
                             , td_font :: XMonadFont
                             , td_paneX :: Integer
                             , td_paneY :: Integer
                             , td_drawingWin :: Window
                             , td_searchString :: String
                             , td_elementmap :: TwoDElementMap a
                             }
generateElementmap :: TwoDState a -> X (TwoDElementMap a)
generateElementmap s = do
    rearrangedElements <- rearranger searchString sortedElements
    return $ zip positions rearrangedElements
  where
    TwoDState {td_availSlots = positions,
               td_gsconfig = gsconfig,
               td_searchString = searchString} = s
    GSConfig {gs_rearranger = rearranger} = gsconfig
    
    filteredElements = L.filter ((searchString `isInfixOfI`) . fst) (td_elements s)
    
    sortedElements = orderElementmap searchString filteredElements
    
    needle `isInfixOfI` haystack = (upper needle) `isInfixOf` (upper haystack)
    upper = map toUpper
orderElementmap :: String  -> [(String,a)] -> [(String,a)]
orderElementmap searchString elements = if not $ null searchString then sortedElements else elements
  where
    upper = map toUpper
    
    calcScore element = ( length $ takeWhile (not . isPrefixOf (upper searchString)) (tails . upper . fst $ element)
                        , element)
    
    
    
    compareScore = comparing (\(score, (str,_)) -> (score, str))
    sortedElements = map snd . sortBy compareScore $ map calcScore elements
newtype TwoD a b = TwoD { unTwoD :: StateT (TwoDState a) X b }
    deriving (Monad,Functor,MonadState (TwoDState a))
instance Applicative (TwoD a) where
    (<*>) = ap
    pure = return
liftX ::  X a1 -> TwoD a a1
liftX = TwoD . lift
evalTwoD ::  TwoD a1 a -> TwoDState a1 -> X a
evalTwoD m s = flip evalStateT s $ unTwoD m
diamondLayer :: (Enum a, Num a, Eq a) => a -> [(a, a)]
diamondLayer 0 = [(0,0)]
diamondLayer n =
  
  
  let tr = [ (x,nx) | x <- [0..n1] ]
      r  = tr ++ (map (\(x,y) -> (y,x)) tr)
  in r ++ (map (negate *** negate) r)
diamond :: (Enum a, Num a, Eq a) => [(a, a)]
diamond = concatMap diamondLayer [0..]
diamondRestrict :: Integer -> Integer -> Integer -> Integer -> [(Integer, Integer)]
diamondRestrict x y originX originY =
  L.filter (\(x',y') -> abs x' <= x && abs y' <= y) .
  map (\(x', y') -> (x' + fromInteger originX, y' + fromInteger originY)) .
  take 1000 $ diamond
findInElementMap :: (Eq a) => a -> [(a, b)] -> Maybe (a, b)
findInElementMap pos = find ((== pos) . fst)
drawWinBox :: Window -> XMonadFont -> (String, String) -> Integer -> Integer -> String -> Integer -> Integer -> Integer -> X ()
drawWinBox win font (fg,bg) ch cw text x y cp =
  withDisplay $ \dpy -> do
  gc <- liftIO $ createGC dpy win
  bordergc <- liftIO $ createGC dpy win
  liftIO $ do
    Just fgcolor <- initColor dpy fg
    Just bgcolor <- initColor dpy bg
    Just bordercolor <- initColor dpy borderColor
    setForeground dpy gc fgcolor
    setBackground dpy gc bgcolor
    setForeground dpy bordergc bordercolor
    fillRectangle dpy win gc (fromInteger x) (fromInteger y) (fromInteger cw) (fromInteger ch)
    drawRectangle dpy win bordergc (fromInteger x) (fromInteger y) (fromInteger cw) (fromInteger ch)
  stext <- shrinkWhile (shrinkIt shrinkText)
           (\n -> do size <- liftIO $ textWidthXMF dpy font n
                     return $ size > (fromInteger (cw(2*cp))))
           text
  printStringXMF dpy win font gc bg fg (fromInteger (x+cp)) (fromInteger (y+(div ch 2))) stext
  liftIO $ freeGC dpy gc
  liftIO $ freeGC dpy bordergc
updateAllElements :: TwoD a ()
updateAllElements =
    do
      s <- get
      updateElements (td_elementmap s)
grayoutElements :: Int -> TwoD a ()
grayoutElements skip =
    do
      s <- get
      updateElementsWithColorizer grayOnly $ drop skip (td_elementmap s)
    where grayOnly _ _ = return ("#808080", "#808080")
updateElements :: TwoDElementMap a -> TwoD a ()
updateElements elementmap = do
      s <- get
      updateElementsWithColorizer (gs_colorizer (td_gsconfig s)) elementmap
updateElementsWithColorizer :: (a -> Bool -> X (String, String)) -> TwoDElementMap a -> TwoD a ()
updateElementsWithColorizer colorizer elementmap = do
    TwoDState { td_curpos = curpos,
                td_drawingWin = win,
                td_gsconfig = gsconfig,
                td_font = font,
                td_paneX = paneX,
                td_paneY = paneY} <- get
    let cellwidth = gs_cellwidth gsconfig
        cellheight = gs_cellheight gsconfig
        paneX' = div (paneXcellwidth) 2
        paneY' = div (paneYcellheight) 2
        updateElement (pos@(x,y),(text, element)) = liftX $ do
            colors <- colorizer element (pos == curpos)
            drawWinBox win font
                       colors
                       cellheight
                       cellwidth
                       text
                       (paneX'+x*cellwidth)
                       (paneY'+y*cellheight)
                       (gs_cellpadding gsconfig)
    mapM_ updateElement elementmap
stdHandle :: Event -> TwoD a (Maybe a) -> TwoD a (Maybe a)
stdHandle (ButtonEvent { ev_event_type = t, ev_x = x, ev_y = y }) contEventloop
    | t == buttonRelease = do
        s @  TwoDState { td_paneX = px, td_paneY = py,
                         td_gsconfig = (GSConfig ch cw _ _ _ _ _ _ _) } <- get
        let gridX = (fi x  (px  cw) `div` 2) `div` cw
            gridY = (fi y  (py  ch) `div` 2) `div` ch
        case lookup (gridX,gridY) (td_elementmap s) of
             Just (_,el) -> return (Just el)
             Nothing -> contEventloop
    | otherwise = contEventloop
stdHandle (ExposeEvent { }) contEventloop = updateAllElements >> contEventloop
stdHandle _ contEventloop = contEventloop
makeXEventhandler :: ((KeySym, String, KeyMask) -> TwoD a (Maybe a)) -> TwoD a (Maybe a)
makeXEventhandler keyhandler = fix $ \me -> join $ liftX $ withDisplay $ \d -> liftIO $ allocaXEvent $ \e -> do
                             maskEvent d (exposureMask .|. keyPressMask .|. buttonReleaseMask) e
                             ev <- getEvent e
                             if ev_event_type ev == keyPress
                               then do
                                  (ks,s) <- lookupString $ asKeyEvent e
                                  return $ do
                                      mask <- liftX $ cleanMask (ev_state ev)
                                      keyhandler (fromMaybe xK_VoidSymbol ks, s, mask)
                               else
                                  return $ stdHandle ev me
shadowWithKeymap :: M.Map (KeyMask, KeySym) a -> ((KeySym, String, KeyMask) -> a) -> (KeySym, String, KeyMask) -> a
shadowWithKeymap keymap dflt keyEvent@(ks,_,m') = fromMaybe (dflt keyEvent) (M.lookup (m',ks) keymap)
select :: TwoD a (Maybe a)
select = do
  s <- get
  return $ fmap (snd . snd) $ findInElementMap (td_curpos s) (td_elementmap s)
cancel :: TwoD a (Maybe a)
cancel = return Nothing
setPos :: (Integer, Integer) -> TwoD a ()
setPos newPos = do
  s <- get
  let elmap = td_elementmap s
      newSelectedEl = findInElementMap newPos (td_elementmap s)
      oldPos = td_curpos s
  when (isJust newSelectedEl && newPos /= oldPos) $ do
    put s { td_curpos = newPos }
    updateElements (catMaybes [(findInElementMap oldPos elmap), newSelectedEl])
move :: (Integer, Integer) -> TwoD a ()
move (dx,dy) = do
  s <- get
  let (x,y) = td_curpos s
      newPos = (x+dx,y+dy)
  setPos newPos
moveNext :: TwoD a ()
moveNext = do
  position <- gets td_curpos
  elems <- gets td_elementmap
  let n = length elems
      m = case findIndex (\p -> fst p == position) elems of
               Nothing -> Nothing
               Just k | k == n1 -> Just 0
                      | otherwise -> Just (k+1)
  whenJust m $ \i ->
      setPos (fst $ elems !! i)
movePrev :: TwoD a ()
movePrev = do
  position <- gets td_curpos
  elems <- gets td_elementmap
  let n = length elems
      m = case findIndex (\p -> fst p == position) elems of
               Nothing -> Nothing
               Just 0  -> Just (n1)
               Just k  -> Just (k1)
  whenJust m $ \i ->
      setPos (fst $ elems !! i)
transformSearchString :: (String -> String) -> TwoD a ()
transformSearchString f = do
          s <- get
          let oldSearchString = td_searchString s
              newSearchString = f oldSearchString
          when (newSearchString /= oldSearchString) $ do
            
            let s' = s { td_searchString = newSearchString }
            m <- liftX $ generateElementmap s'
            let s'' = s' { td_elementmap = m }
                oldLen = length $ td_elementmap s
                newLen = length $ td_elementmap s''
            
            
            
            when (newLen < oldLen) $ grayoutElements newLen
            put s''
            updateAllElements
defaultNavigation :: TwoD a (Maybe a)
defaultNavigation = makeXEventhandler $ shadowWithKeymap navKeyMap navDefaultHandler
  where navKeyMap = M.fromList [
           ((0,xK_Escape)     , cancel)
          ,((0,xK_Return)     , select)
          ,((0,xK_slash)      , substringSearch defaultNavigation)
          ,((0,xK_Left)       , move (1,0) >> defaultNavigation)
          ,((0,xK_h)          , move (1,0) >> defaultNavigation)
          ,((0,xK_Right)      , move (1,0) >> defaultNavigation)
          ,((0,xK_l)          , move (1,0) >> defaultNavigation)
          ,((0,xK_Down)       , move (0,1) >> defaultNavigation)
          ,((0,xK_j)          , move (0,1) >> defaultNavigation)
          ,((0,xK_Up)         , move (0,1) >> defaultNavigation)
          ,((0,xK_k)          , move (0,1) >> defaultNavigation)
          ,((0,xK_Tab)        , moveNext >> defaultNavigation)
          ,((0,xK_n)          , moveNext >> defaultNavigation)
          ,((shiftMask,xK_Tab), movePrev >> defaultNavigation)
          ,((0,xK_p)          , movePrev >> defaultNavigation)
          ]
        
        navDefaultHandler = const defaultNavigation
navNSearch :: TwoD a (Maybe a)
navNSearch = makeXEventhandler $ shadowWithKeymap navNSearchKeyMap navNSearchDefaultHandler
  where navNSearchKeyMap = M.fromList [
           ((0,xK_Escape)     , cancel)
          ,((0,xK_Return)     , select)
          ,((0,xK_Left)       , move (1,0) >> navNSearch)
          ,((0,xK_Right)      , move (1,0) >> navNSearch)
          ,((0,xK_Down)       , move (0,1) >> navNSearch)
          ,((0,xK_Up)         , move (0,1) >> navNSearch)
          ,((0,xK_Tab)        , moveNext >> navNSearch)
          ,((shiftMask,xK_Tab), movePrev >> navNSearch)
          ,((0,xK_BackSpace), transformSearchString (\s -> if (s == "") then "" else init s) >> navNSearch)
          ]
        
        navNSearchDefaultHandler (_,s,_) = do
          transformSearchString (++ s)
          navNSearch
substringSearch :: TwoD a (Maybe a) -> TwoD a (Maybe a)
substringSearch returnNavigation = fix $ \me ->
  let searchKeyMap = M.fromList [
           ((0,xK_Escape)   , transformSearchString (const "") >> returnNavigation)
          ,((0,xK_Return)   , returnNavigation)
          ,((0,xK_BackSpace), transformSearchString (\s -> if (s == "") then "" else init s) >> me)
          ]
      searchDefaultHandler (_,s,_) = do
          transformSearchString (++ s)
          me
  in makeXEventhandler $ shadowWithKeymap searchKeyMap searchDefaultHandler
hsv2rgb :: Fractional a => (Integer,a,a) -> (a,a,a)
hsv2rgb (h,s,v) =
    let hi = (div h 60) `mod` 6 :: Integer
        f = (((fromInteger h)/60)  (fromInteger hi)) :: Fractional a => a
        q = v * (1f)
        p = v * (1s)
        t = v * (1(1f)*s)
    in case hi of
         0 -> (v,t,p)
         1 -> (q,v,p)
         2 -> (p,v,t)
         3 -> (p,q,v)
         4 -> (t,p,v)
         5 -> (v,p,q)
         _ -> error "The world is ending. x mod a >= a."
stringColorizer :: String -> Bool -> X (String, String)
stringColorizer s active =
    let seed x = toInteger (sum $ map ((*x).fromEnum) s) :: Integer
        (r,g,b) = hsv2rgb ((seed 83) `mod` 360,
                           (fromInteger ((seed 191) `mod` 1000))/2500+0.4,
                           (fromInteger ((seed 121) `mod` 1000))/2500+0.4)
    in if active
         then return ("#faff69", "black")
         else return ("#" ++ concat (map (twodigitHex.(round :: Double -> Word8).(*256)) [r, g, b] ), "white")
fromClassName :: Window -> Bool -> X (String, String)
fromClassName w active = runQuery className w >>= flip defaultColorizer active
twodigitHex :: Word8 -> String
twodigitHex a = printf "%02x" a
colorRangeFromClassName :: (Word8, Word8, Word8) 
                        -> (Word8, Word8, Word8) 
                        -> (Word8, Word8, Word8) 
                        -> (Word8, Word8, Word8) 
                        -> (Word8, Word8, Word8) 
                        -> Window -> Bool -> X (String, String)
colorRangeFromClassName startC endC activeC inactiveT activeT w active =
    do classname <- runQuery className w
       if active
         then return (rgbToHex activeC, rgbToHex activeT)
         else return (rgbToHex $ mix startC endC
                  $ stringToRatio classname, rgbToHex inactiveT)
    where rgbToHex :: (Word8, Word8, Word8) -> String
          rgbToHex (r, g, b) = '#':twodigitHex r
                               ++twodigitHex g++twodigitHex b
mix :: (Word8, Word8, Word8) -> (Word8, Word8, Word8)
        -> Double -> (Word8, Word8, Word8)
mix (r1, g1, b1) (r2, g2, b2) r = (mix' r1 r2, mix' g1 g2, mix' b1 b2)
    where  mix' a b = truncate $ (fi a * r) + (fi b * (1  r))
stringToRatio :: String -> Double
stringToRatio "" = 0
stringToRatio s = let gen = mkStdGen $ sum $ map fromEnum s
                      range = (\(a, b) -> b  a) $ genRange gen
                      randomInt = foldr1 combine $ replicate 20 next
                      combine f1 f2 g = let (_, g') = f1 g in f2 g'
                  in fi (fst $ randomInt gen) / fi range
gridselect :: GSConfig a -> [(String,a)] -> X (Maybe a)
gridselect _ [] = return Nothing
gridselect gsconfig elements =
 withDisplay $ \dpy -> do
    rootw <- asks theRoot
    scr <- gets $ screenRect . W.screenDetail . W.current . windowset
    win <- liftIO $ mkUnmanagedWindow dpy (defaultScreenOfDisplay dpy) rootw
                    (rect_x scr) (rect_y scr) (rect_width scr) (rect_height scr)
    liftIO $ mapWindow dpy win
    liftIO $ selectInput dpy win (exposureMask .|. keyPressMask .|. buttonReleaseMask)
    status <- io $ grabKeyboard dpy win True grabModeAsync grabModeAsync currentTime
    io $ grabButton dpy button1 anyModifier win True buttonReleaseMask grabModeAsync grabModeAsync none none
    font <- initXMF (gs_font gsconfig)
    let screenWidth = toInteger $ rect_width scr
        screenHeight = toInteger $ rect_height scr
    selectedElement <- if (status == grabSuccess) then do
                            let restriction ss cs = (fromInteger ss/fromInteger (cs gsconfig)1)/2 :: Double
                                restrictX = floor $ restriction screenWidth gs_cellwidth
                                restrictY = floor $ restriction screenHeight gs_cellheight
                                originPosX = floor $ ((gs_originFractX gsconfig)  (1/2)) * 2 * fromIntegral restrictX
                                originPosY = floor $ ((gs_originFractY gsconfig)  (1/2)) * 2 * fromIntegral restrictY
                                coords = diamondRestrict restrictX restrictY originPosX originPosY
                                s = TwoDState { td_curpos = (head coords),
                                                td_availSlots = coords,
                                                td_elements = elements,
                                                td_gsconfig = gsconfig,
                                                td_font = font,
                                                td_paneX = screenWidth,
                                                td_paneY = screenHeight,
                                                td_drawingWin = win,
                                                td_searchString = "",
                                                td_elementmap = [] }
                            m <- generateElementmap s
                            evalTwoD (updateAllElements >> (gs_navigate gsconfig))
                                     (s { td_elementmap = m })
                      else
                          return Nothing
    liftIO $ do
      unmapWindow dpy win
      destroyWindow dpy win
      sync dpy False
    releaseXMF font
    return selectedElement
gridselectWindow :: GSConfig Window -> X (Maybe Window)
gridselectWindow gsconf = windowMap >>= gridselect gsconf
withSelectedWindow :: (Window -> X ()) -> GSConfig Window -> X ()
withSelectedWindow callback conf = do
    mbWindow <- gridselectWindow conf
    case mbWindow of
        Just w -> callback w
        Nothing -> return ()
windowMap :: X [(String,Window)]
windowMap = do
    ws <- gets windowset
    wins <- mapM keyValuePair (W.allWindows ws)
    return wins
 where keyValuePair w = flip (,) w `fmap` decorateName' w
decorateName' :: Window -> X String
decorateName' w = do
  fmap show $ getName w
buildDefaultGSConfig :: (a -> Bool -> X (String,String)) -> GSConfig a
buildDefaultGSConfig col = GSConfig 50 130 10 col "xft:Sans-8" defaultNavigation noRearranger (1/2) (1/2)
borderColor :: String
borderColor = "white"
bringSelected :: GSConfig Window -> X ()
bringSelected = withSelectedWindow $ \w -> do
    windows (bringWindow w)
    XMonad.focus w
    windows W.shiftMaster
goToSelected :: GSConfig Window -> X ()
goToSelected = withSelectedWindow $ windows . W.focusWindow
spawnSelected :: GSConfig String -> [String] -> X ()
spawnSelected conf lst = gridselect conf (zip lst lst) >>= flip whenJust spawn
runSelectedAction :: GSConfig (X ()) -> [(String, X ())] -> X ()
runSelectedAction conf actions = do
    selectedActionM <- gridselect conf actions
    case selectedActionM of
        Just selectedAction -> selectedAction
        Nothing -> return ()
gridselectWorkspace :: GSConfig WorkspaceId ->
                          (WorkspaceId -> WindowSet -> WindowSet) -> X ()
gridselectWorkspace conf viewFunc = gridselectWorkspace' conf (windows . viewFunc)
gridselectWorkspace' :: GSConfig WorkspaceId -> (WorkspaceId -> X ()) -> X ()
gridselectWorkspace' conf func = withWindowSet $ \ws -> do
    let wss = map W.tag $ W.hidden ws ++ map W.workspace (W.current ws : W.visible ws)
    gridselect conf (zip wss wss) >>= flip whenJust func
type Rearranger a = String -> [(String, a)] -> X [(String, a)]
noRearranger :: Rearranger a
noRearranger _ = return
searchStringRearrangerGenerator :: (String -> a) -> Rearranger a
searchStringRearrangerGenerator f =
    let r "" xs                       = return $ xs
        r s  xs | s `elem` map fst xs = return $ xs
                | otherwise           = return $ xs ++ [(s, f s)]
    in r