{-# OPTIONS_GHC -fno-warn-name-shadowing -fno-warn-unused-binds #-}
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses
  , PatternGuards, ExistentialQuantification
  , FlexibleContexts #-}

-----------------------------------------------------------------------------
-- |
-- Module      :  XMonad.Layout.ZoomRow
-- Description :  Row layout with individually resizable elements.
-- Copyright   :  Quentin Moser <moserq@gmail.com>
-- License     :  BSD-style (see LICENSE)
--
-- Maintainer  :  orphaned
-- Stability   :  unstable
-- Portability :  unportable
--
-- Row layout with individually resizable elements.
--
-----------------------------------------------------------------------------

module XMonad.Layout.ZoomRow ( -- * Usage
                               -- $usage
                               ZoomRow
                               -- * Creation
                             , zoomRow
                               -- * Messages
                             , ZoomMessage(..)
                             , zoomIn
                             , zoomOut
                             , zoomReset
                               -- * Use with non-'Eq' elements
                               -- $noneq
                             , zoomRowWith
                             , EQF(..)
                             , ClassEQ(..)
                             ) where

import XMonad
import XMonad.Prelude (fromMaybe, fi)
import qualified XMonad.StackSet as W

import XMonad.Util.Stack

import Control.Arrow (second)

-- $usage
-- This module provides a layout which places all windows in a single
-- row; the size occupied by each individual window can be increased
-- and decreased, and a window can be set to use the whole available
-- space whenever it has focus.
--
-- You can use this module by including  the following in your @xmonad.hs@:
--
-- > import XMonad.Layout.ZoomRow
--
-- and using 'zoomRow' somewhere in your 'layoutHook', for example:
--
-- > myLayout = zoomRow ||| Mirror zoomRow
--
-- To be able to resize windows, you can create keybindings to send
-- the relevant 'ZoomMessage's:
--
-- >   -- Increase the size occupied by the focused window
-- > , ((modMask .|. shifMask, xK_minus), sendMessage zoomIn)
-- >   -- Decrease the size occupied by the focused window
-- > , ((modMayk             , xK_minus), sendMessage zoomOut)
-- >   -- Reset the size occupied by the focused window
-- > , ((modMask             , xK_equal), sendMessage zoomReset)
-- >   -- (Un)Maximize the focused window
-- > , ((modMask             , xK_f    ), sendMessage ToggleZoomFull)
--
-- For more information on editing your layoutHook and key bindings,
-- see <https://xmonad.org/TUTORIAL.html#customizing-xmonad the tutorial>
-- and "XMonad.Doc.Extending".

-- * Creation functions

-- | 'ZoomRow' layout for laying out elements which are instances of
-- 'Eq'. Perfect for 'Window's.
zoomRow :: (Eq a, Show a, Read a) => ZoomRow ClassEQ a
zoomRow = ZC ClassEQ emptyZ

-- $noneq
-- Haskell's 'Eq' class is usually concerned with structural equality, whereas
-- what this layout really wants is for its elements to have a unique identity,
-- even across changes. There are cases (such as, importantly, 'Window's) where
-- the 'Eq' instance for a type actually does that, but if you want to lay
-- out something more exotic than windows and your 'Eq' means something else,
-- you can use the following.

-- | ZoomRow layout with a custom equality predicate. It should
-- of course satisfy the laws for 'Eq', and you should also make
-- sure that the layout never has to handle two \"equal\" elements
-- at the same time (it won't do any huge damage, but might behave
-- a bit strangely).
zoomRowWith :: (EQF f a, Show (f a), Read (f a), Show a, Read a)
               => f a -> ZoomRow f a
zoomRowWith f = ZC f emptyZ


-- * The datatypes

-- | A layout that arranges its windows in a horizontal row,
-- and allows to change the relative size of each element
-- independently.
data ZoomRow f a = ZC { zoomEq ::  f a
                          -- ^ Function to compare elements for
                          -- equality, a real Eq instance might
                          -- not be what you want in some cases
                      , zoomRatios :: Zipper (Elt a)
                          -- ^ Element specs. The zipper is so we
                          -- know what the focus is when we handle
                          --  a message
                      }
  deriving (Show, Read, Eq)

-- | Class for equivalence relations. Must be transitive, reflexive.
class EQF f a where
    eq :: f a -> a -> a -> Bool

-- | To use the usual '==':
data ClassEQ a = ClassEQ
  deriving (Show, Read, Eq)

instance Eq a => EQF ClassEQ a where
    eq _ a b = a == b

-- | Size specification for an element.
data Elt a = E { elt :: a -- ^ The element
               , ratio :: Rational -- ^ Its size ratio
               , full :: Bool -- ^ Whether it should occupy all the
                              -- available space when it has focus.
               }
  deriving (Show, Read, Eq)


-- * Helpers

getRatio :: Elt a -> (a, Rational)
getRatio (E a r _) = (a,r)

lookupBy :: (a -> a -> Bool) -> a -> [Elt a] -> Maybe (Elt a)
lookupBy _ _ [] = Nothing
lookupBy f a (E a' r b : _) | f a a' = Just $ E a r b
lookupBy f a (_:es) = lookupBy f a es

setFocus :: Zipper a -> a -> Zipper a
setFocus Nothing a = Just $ W.Stack a [] []
setFocus (Just s) a = Just s { W.focus = a }


-- * Messages

-- | The type of messages accepted by a 'ZoomRow' layout
data ZoomMessage = Zoom Rational
                 -- ^ Multiply the focused window's size factor
                 -- by the given number.
                 | ZoomTo Rational
                 -- ^ Set the focused window's size factor to the
                 -- given number.
                 | ZoomFull Bool
                 -- ^ Set whether the focused window should occupy
                 -- all available space when it has focus
                 | ZoomFullToggle
                 -- ^ Toggle whether the focused window should
                 -- occupy all available space when it has focus
  deriving (Show)

instance Message ZoomMessage

-- | Increase the size of the focused window.
-- Defined as @Zoom 1.5@
zoomIn :: ZoomMessage
zoomIn = Zoom 1.5

-- | Decrease the size of the focused window.
-- Defined as @Zoom (2/3)@
zoomOut :: ZoomMessage
zoomOut = Zoom $ 2/3

-- | Reset the size of the focused window.
-- Defined as @ZoomTo 1@
zoomReset :: ZoomMessage
zoomReset = ZoomTo 1


-- * LayoutClass instance

instance (EQF f a, Show a, Read a, Show (f a), Read (f a), Typeable f)
    => LayoutClass (ZoomRow f) a where
    description (ZC _ Nothing) = "ZoomRow"
    description (ZC _ (Just s)) = "ZoomRow" ++ if full $ W.focus s
                                                then " (Max)"
                                                else ""

    emptyLayout (ZC _ Nothing) _ = return ([], Nothing)
    emptyLayout (ZC f _) _ = return ([], Just $ ZC f Nothing)

    doLayout (ZC f zelts) r@(Rectangle _ _ w _) s
        = let elts = W.integrate' zelts
              zelts' = mapZ_ (\a -> fromMaybe (E a 1 False)
                                    $ lookupBy (eq f) a elts) $ Just s
              elts' = W.integrate' zelts'

              maybeL' = if zelts `noChange` zelts'
                          then Nothing
                          else Just $ ZC f zelts'

              total = sum  $ map ratio elts'

              widths =  map (second ((* fi w) . (/total)) . getRatio) elts'

          in case getFocusZ zelts' of
               Just (E a _ True) -> return ([(a, r)], maybeL')
               _ -> return (makeRects r widths, maybeL')

        where makeRects :: Rectangle -> [(a, Rational)] -> [(a, Rectangle)]
              makeRects r pairs = let as = map fst pairs
                                      widths = map snd pairs
                                      discreteWidths = snd $ foldr discretize (0, []) widths
                                      rectangles = snd $ foldr makeRect (r, []) discreteWidths
                                  in zip as rectangles

              -- | Make a new rectangle by substracting the given width from the available
              -- space (from the right, since this is a foldr)
              makeRect :: Dimension -> (Rectangle, [Rectangle]) -> (Rectangle, [Rectangle])
              makeRect w (Rectangle x y w0 h, rs) = ( Rectangle x y (w0-w) h
                                                    , Rectangle (x+fi w0-fi w) y w h : rs )

              -- | Round a list of fractions in a way that maintains the total.
              -- If you know a better way to do this I'm very interested.
              discretize :: Rational -> (Rational, [Dimension]) -> (Rational, [Dimension])
              discretize r (carry, ds) = let (d, carry') = properFraction $ carry+r
                                         in (carry', d:ds)

              noChange z1 z2 = toTags z1 `helper` toTags z2
                  where helper [] [] = True
                        helper (Right a:as) (Right b:bs) = a `sameAs` b && as `helper` bs
                        helper (Left a:as) (Left b:bs) = a `sameAs` b && as `helper` bs
                        helper _ _ = False
                        E a1 r1 b1 `sameAs` E a2 r2 b2 = eq f a1 a2 && (r1 == r2) && (b1 == b2)

    pureMessage (ZC f zelts) sm | Just (ZoomFull False) <- fromMessage sm
                                , Just (E a r True) <- getFocusZ zelts
        = Just $ ZC f $ setFocus zelts $ E a r False

    pureMessage (ZC f zelts) sm | Just (ZoomFull True) <- fromMessage sm
                                , Just (E a r False) <- getFocusZ zelts
        = Just $ ZC f $ setFocus zelts $ E a r True

    pureMessage (ZC f zelts) sm | Just (E a r b) <- getFocusZ zelts
        = case fromMessage sm of
            Just (Zoom r') -> Just $ ZC f $ setFocus zelts $ E a (r*r') b
            Just (ZoomTo r') -> Just $ ZC f $ setFocus zelts $ E a r' b
            Just ZoomFullToggle -> pureMessage (ZC f zelts)
                                     $ SomeMessage $ ZoomFull $ not b
            _ -> Nothing

    pureMessage _ _ = Nothing
