{-# LANGUAGE DeriveAnyClass  #-}
{-# LANGUAGE DeriveGeneric   #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}

{-|

Copyright:
  This file is part of the package zxcvbn-hs. It is subject to the
  license terms in the LICENSE file found in the top-level directory
  of this distribution and at:

    https://code.devalot.com/sthenauth/zxcvbn-hs

  No part of this package, including this file, may be copied,
  modified, propagated, or distributed except according to the terms
  contained in the LICENSE file.

License: MIT

-}
module Text.Password.Strength.Internal.Adjacency (
  -- * Adjacency Matching (for Keyboard Patterns)
  Pattern,
  Direction(..),
  Move(..),
  Layer(..),
  Adjacency(..),
  AdjacencyTable(..),
  totalChars,
  averageNeighbors,
  patterns,
  findSequence,
  AdjacencyScore(..),
  patternLength,
  totalTurns,
  primaryLayer,
  secondaryLayer,
  scoreSequence
  ) where

--------------------------------------------------------------------------------
-- Library Imports:
import Control.Lens ((&), (^.), (+~), (.~))
import Control.Lens.TH (makeLenses)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Binary (Binary)
import Data.Text (Text)
import qualified Data.Text as Text
import GHC.Generics (Generic)

--------------------------------------------------------------------------------
-- | A @Pattern@ is two Unicode characters next to one another in a password.
type Pattern = (Char, Char)

--------------------------------------------------------------------------------
-- | Direction of movement for adjacent characters.
data Direction = N | NE | E | SE | S | SW | W | NW
  deriving (Generic, Binary, Show, Eq, Ord, Enum, Bounded)

--------------------------------------------------------------------------------
-- | Movement between characters.
data Move = Move Direction | Stay
  deriving (Generic, Binary, Show, Eq)

--------------------------------------------------------------------------------
-- | Keyboard layers.
data Layer = Primary | Secondary
  deriving (Generic, Binary, Show, Eq, Ord, Enum, Bounded)

--------------------------------------------------------------------------------
-- | Information about how two characters are related to one another.
data Adjacency = Adjacency
  { _movement :: Move
    -- ^ The direction moving from the first to second character.

  , _firstLayer :: Layer
    -- ^ The layer that the first character is on.

  , _secondLayer :: Layer
    -- ^ The layer that the second character is on.
  }
  deriving (Generic, Binary, Show)

makeLenses ''Adjacency

--------------------------------------------------------------------------------
-- | An adjacency graph (usually representing a single keyboard).
data AdjacencyTable = AdjacencyTable
  { _totalChars :: Int
    -- ^ Total number of characters in the graph (total keys on the
    -- keyboard including all layers).

  , _averageNeighbors :: Int
    -- ^ Average number of neighbors in the graph.

  , _patterns :: Map Pattern Adjacency
    -- ^ Dictionary for looking up patterns.

  } deriving (Generic, Binary, Show)

makeLenses ''AdjacencyTable

--------------------------------------------------------------------------------
-- | Find a pattern if it exists.  If all characters in the given
-- 'Text' form a pattern in the given 'Graph' then a list of matches
-- will be returned.
findSequence :: Text -> AdjacencyTable -> Maybe (NonEmpty Adjacency)
findSequence t AdjacencyTable{..} =
  let chars = Text.unpack t
      ms = mapM (`Map.lookup` _patterns) (zip chars (drop 1 chars))
  in NonEmpty.fromList <$> ms

--------------------------------------------------------------------------------
-- | Scoring information for adjacent characters.
data AdjacencyScore = AdjacencyScore
  { _patternLength :: Int
    -- ^ Number of characters in the pattern.

  , _totalTurns :: Int
    -- ^ Total number of turns needed.

  , _primaryLayer :: Int
    -- ^ Characters that are on the primary layer.

  , _secondaryLayer :: Int
    -- ^ Characters that are on a secondary layer.

  , _lastMovement :: Move
    -- ^ The direction on the last character.

  } deriving (Show, Eq)

makeLenses ''AdjacencyScore

--------------------------------------------------------------------------------
instance Semigroup AdjacencyScore where
  (<>) (AdjacencyScore l t p s m) (AdjacencyScore l' t' p' s' _) =
    AdjacencyScore (l+l') (t+t') (p+p') (s+s') m

--------------------------------------------------------------------------------
instance Monoid AdjacencyScore where
  mempty = AdjacencyScore 0 0 0 0 Stay

--------------------------------------------------------------------------------
-- | Calculate the score for two adjacent characters.
scoreSequence :: AdjacencyScore -> Adjacency -> AdjacencyScore
scoreSequence s a =
  s & turns
    & layers
    & patternLength +~ (if (s ^. patternLength) == 0 then 2 else 1)
    & lastMovement  .~ (a ^. movement)

  where
    turns :: AdjacencyScore -> AdjacencyScore
    turns = if (a ^. movement) /= (s ^. lastMovement)
              then totalTurns +~ 1
              else id

    -- Usually we focus on the layer of the second character but when
    -- we are looking at the start of the pattern we need to consider
    -- both characters.
    layers :: AdjacencyScore -> AdjacencyScore
    layers = if (s ^. patternLength) == 0
               then layer (a ^. firstLayer) . (& layer (a ^. secondLayer))
               else layer (a ^. secondLayer)

    layer :: Layer -> AdjacencyScore -> AdjacencyScore
    layer Primary   = primaryLayer   +~ 1
    layer Secondary = secondaryLayer +~ 1