--------------------------------------------------------------------------------
-- | This module is useful for aligning things.
module Language.Haskell.Stylish.Align
    ( Alignable (..)
    , align
    ) where


--------------------------------------------------------------------------------
import           Data.List                       (nub)
import qualified SrcLoc                          as S


--------------------------------------------------------------------------------
import           Language.Haskell.Stylish.Editor
import           Language.Haskell.Stylish.Util


--------------------------------------------------------------------------------
-- | This represent a single line which can be aligned.  We have something on
-- the left and the right side, e.g.:
--
-- > [x]  -> x + 1
-- > ^^^^    ^^^^^
-- > LEFT    RIGHT
--
-- We also have the container which holds the entire line:
--
-- > [x]  -> x + 1
-- > ^^^^^^^^^^^^^
-- > CONTAINER
--
-- And then we have a "right lead" which is just represented by an 'Int', since
-- @haskell-src-exts@ often does not allow us to access it.  In the example this
-- is:
--
-- > [x]  -> x + 1
-- >      ^^^
-- >      RLEAD
--
-- This info is enough to align a bunch of these lines.  Users of this module
-- should construct a list of 'Alignable's representing whatever they want to
-- align, and then call 'align' on that.
data Alignable a = Alignable
    { aContainer :: !a
    , aLeft      :: !a
    , aRight     :: !a
    -- | This is the minimal number of columns we need for the leading part not
    -- included in our right string.  For example, for datatype alignment, this
    -- leading part is the string ":: " so we use 3.
    , aRightLead :: !Int
    } deriving (Show)

--------------------------------------------------------------------------------
-- | Create changes that perform the alignment.

align
  :: Maybe Int                    -- ^ Max columns
  -> [Alignable S.RealSrcSpan]    -- ^ Alignables
  -> [Change String]              -- ^ Changes performing the alignment
align _ [] = []
align maxColumns alignment
  -- Do not make an changes if we would go past the maximum number of columns
  | exceedsColumns (longestLeft + longestRight)  = []
  | not (fixable alignment)                      = []
  | otherwise                                    = map align' alignment
  where
    exceedsColumns i = case maxColumns of
      Nothing -> False
      Just c  -> i > c

    -- The longest thing in the left column
    longestLeft = maximum $ map (S.srcSpanEndCol . aLeft) alignment

    -- The longest thing in the right column
    longestRight = maximum
      [ S.srcSpanEndCol (aRight a) - S.srcSpanStartCol (aRight a)
          + aRightLead a
      | a <- alignment
      ]

    align' a = changeLine (S.srcSpanStartLine $ aContainer a) $ \str ->
      let column = S.srcSpanEndCol $ aLeft a
          (pre, post) = splitAt column str
      in [padRight longestLeft (trimRight pre) ++ trimLeft post]

--------------------------------------------------------------------------------
-- | Checks that all the alignables appear on a single line, and that they do
-- not overlap.

fixable :: [Alignable S.RealSrcSpan] -> Bool
fixable []     = False
fixable [_]    = False
fixable fields = all singleLine containers && nonOverlapping containers
  where
    containers        = map aContainer fields
    singleLine s      = S.srcSpanStartLine s == S.srcSpanEndLine s
    nonOverlapping ss = length ss == length (nub $ map S.srcSpanStartLine ss)