{-# LANGUAGE DeriveDataTypeable #-}

module Data.Text.Punycode.Decode (PunycodeDecodeException (..), decode) where

import           Control.Exception.Base
import qualified Data.ByteString as BS
import           Data.Char
import           Data.Serialize hiding (decode)
import qualified Data.Text as T
import           Data.Typeable
import           Data.Word

import           Data.Text.Punycode.Shared

data PunycodeDecodeException
       = GenericDecodeException
       | InternalStringTooShort
       | InputTooShort
       | RightOfHyphenShouldBeAlphanumeric
       | LeftOfHyphenShouldBeBasic
       | CantStartWithDash
       | InvalidCodePoint
    deriving (Eq,Show,Typeable)

instance Exception PunycodeDecodeException

-- | Decode a string into its unicode form
decode :: BS.ByteString -> Either PunycodeDecodeException T.Text
decode input
  | input == BS.pack [45, 45] = Right $ T.pack "-"
  | not (BS.null input) && BS.length (BS.filter (== 45) input) == 1 && BS.head input == 45 = Left CantStartWithDash
  | T.any (not . isExtendedBasic) before = Left LeftOfHyphenShouldBeBasic
  | otherwise = case runGet (inner2 initial_n 0 initial_bias before) after of
      Right out -> out
      Left _ -> Left InputTooShort
  where (before, after)
          | BS.any f input = (T.pack $ map (chr . fromIntegral) $ BS.unpack $ BS.init b1, a1)
          | otherwise = (T.empty, input)
        f = (== (fromIntegral $ ord '-'))
        (b1, a1) = BS.breakEnd f input

inner2 :: Int -> Int -> Int -> T.Text -> Get (Either PunycodeDecodeException T.Text)
inner2 n oldi bias output = do
  b <- isEmpty
  helper b
  where helper False = do
          i <- inner base 1 oldi bias
          helper' i
          where helper' Nothing = return $ Left RightOfHyphenShouldBeAlphanumeric
                helper' (Just i) = case output' of
                  Right output'' -> inner2 n' (i' + 1) bias' output''
                  Left err -> return $ Left err
                  where bias' = adapt (i - oldi) (T.length output + 1) (oldi == 0)
                        n' = n + i `div` (T.length output + 1)
                        i' = i `mod` (T.length output + 1)
                        output' = insertInto output n' i'
        helper True = return $ Right output

inner :: Int -> Int -> Int -> Int -> Get (Maybe Int)
inner k w i bias = do
  word8 <- getWord8
  helper $ word8ToDigit word8
  where helper Nothing = return Nothing
        helper (Just digit)
          | digit < t = return $ Just i'
          | otherwise = inner (k + base) w' i' bias
          where w' = w * (base - t)
                i' = i + digit * w
                t
                  | k <= bias + tmin = tmin
                  | k >= bias + tmax = tmax
                  | otherwise = k - bias

insertInto :: T.Text -> Int -> Int -> Either PunycodeDecodeException T.Text
insertInto input n i
  | T.length input < i = Left InternalStringTooShort
  | otherwise = case n' of
    Just n'' -> Right $ T.concat [T.take i input, T.singleton n'', T.drop i input]
    Nothing -> Left InvalidCodePoint
  where n' = safeChr n

safeChr :: Int -> Maybe Char
safeChr x
  | x >= 0 && x <= fromEnum (maxBound :: Char) = Just $ chr x
  | otherwise = Nothing

word8ToDigit :: Word8 -> Maybe Int
word8ToDigit = helper . fromIntegral
  where helper word8
          | word8 >= ord 'a' && word8 <= ord 'z' = Just $ word8 - (ord 'a')
          | word8 >= ord 'A' && word8 <= ord 'Z' = Just $ word8 - (ord 'A')
          | word8 >= ord '0' && word8 <= ord '9' = Just $ 26 + word8 - (ord '0')
          | otherwise = Nothing

isExtendedBasic :: Char -> Bool
isExtendedBasic x
  | isBasic x = True
  | ord x == 128 = True
  | otherwise = False
