{-# LANGUAGE DeriveAnyClass             #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE ViewPatterns               #-}


module Shpadoinkle.Widgets.Types.Search where


import           Data.Aeson        (FromJSON, ToJSON)
import           Data.Foldable     as F (Foldable (foldl'))
import           Data.List         (sort)
import           Data.Maybe        (mapMaybe)
import           Data.String       (IsString)
import           Data.Text         (Text, isInfixOf, splitOn, strip, toLower,
                                    unpack)
import           GHC.Generics      (Generic)
import           Shpadoinkle       (NFData)
import           Text.EditDistance (defaultEditCosts, levenshteinDistance)


newtype Search = Search { unSearch :: Text }
  deriving newtype (Eq, Ord, Show, Read, IsString, Semigroup, Monoid, ToJSON, FromJSON)
  deriving stock Generic
  deriving anyclass NFData


newtype EditDistance = EditDistance { unEditDistance :: Int }
  deriving newtype (Eq, Ord, Show, Read, ToJSON, FromJSON)
  deriving stock Generic
  deriving anyclass NFData


data Levenshtiened a = Levenshtiened { _distance :: !EditDistance, _unLevenshtiened :: a } deriving (Eq, Show, Read, Generic, NFData)
instance Eq       a => Ord    (Levenshtiened a) where
  compare (Levenshtiened x _) (Levenshtiened y _) = unEditDistance x `compare` unEditDistance y


mkLevenshtiened :: Text -> Search -> a -> Levenshtiened a
mkLevenshtiened  t (Search s) =
  Levenshtiened . EditDistance $ levenshteinDistance defaultEditCosts (prep s) (prep t)
  where prep = unpack . strip


forgivingly :: Search -> Text -> Bool
forgivingly (Search (strip -> "")) _ = True
forgivingly (Search s) haystack = Prelude.all test . splitOn " " $ strip s
  where test ""     = False
        test needle = forgive needle `isInfixOf` forgive haystack
        forgive     = toLower . strip


concatFuzzy :: [a -> Text] -> a -> Text
concatFuzzy = F.foldl' (\f g a -> f a <> " " <> g a) (const "")


fuzzySearch :: Ord a => [a -> Text] -> Search -> [a] -> [a]
fuzzySearch toChunks s = fmap _unLevenshtiened . sort .
  mapMaybe (\x -> let hay = concatFuzzy toChunks x
                  in if forgivingly s hay
                     then Just $ mkLevenshtiened hay s x
                     else Nothing
           )