module Escoger.Matches
( sortByScore
, score
, matchLength
, findEndOfMatch
) where

import           Control.Applicative
import           Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import           Data.Char (ord, chr)
import           Data.Maybe (mapMaybe)
import           Data.Ord (comparing)
import           Data.Vector (Vector)
import qualified Data.Vector as V
import qualified Data.Vector.Algorithms.Intro as VA
import           GHC.Unicode (isAsciiUpper)

sortByScore :: ByteString -> Vector ByteString -> Vector ByteString
sortByScore query choices = sortFilter choiceScores
  where
    sortFilter = V.map fst . V.modify (VA.sortBy $ comparing (negate . snd)) . V.filter (\(_,y) -> y /= 0)
    choiceScores = V.map (\x -> (x, score x query)) choices

score :: ByteString -> ByteString -> Double
score choice query
  | B.null query = 1
  | B.null choice = 0
  | otherwise = case matchLength c q of
    Nothing -> 0
    Just len -> let tmpScore = fromIntegral (B.length q) / fromIntegral len
                    choiceLength = fromIntegral $ B.length c
                in tmpScore / choiceLength
    where
      c = BC.map toAsciiLower choice
      q = BC.map toAsciiLower query

matchLength :: ByteString -> ByteString -> Maybe Int
matchLength choice query =
  if null matches then Nothing else Just $ minimum matches
  where
    matches :: [Int]
    matches = (filter (0<=) . mapMaybe findNorm) indices
    findNorm i = findEndOfMatch choice query i >>= normalize i
    normalize i m = return $ 1 + subtract i m
    indices :: [Int]
    indices = B.elemIndices (B.head query) choice

findEndOfMatch :: ByteString -> ByteString -> Int -> Maybe Int
findEndOfMatch choice query index =
  B.foldl' func (Just index) (B.tail query)
  where
    func m_lastIndex q = let findNextMatch i = B.findIndex (q ==) (B.drop (i+1) choice)
                             acc = ((+) <$> ((+1) <$> m_lastIndex) <*>)
                    in m_lastIndex >>= findNextMatch >>= acc . return

toAsciiLower :: Char -> Char
toAsciiLower c = if isAsciiUpper c
                   then chr (ord c + 32)
                   else c