-----------------------------------------------------------------------------
-- |
-- Copyright   : (C) 2015 Dimitri Sabadie
-- License     : BSD3
--
-- Maintainer  : Dimitri Sabadie <dimitri.sabadie@gmail.com>
-- Stability   : experimental
-- Portability : portable
--
-----------------------------------------------------------------------------

module Codec.Wavefront.Token where

import Codec.Wavefront.Face
import Codec.Wavefront.Line
import Codec.Wavefront.Location
import Codec.Wavefront.Normal
import Codec.Wavefront.Point
import Codec.Wavefront.TexCoord
import Control.Applicative ( Alternative(..) )
import Data.Attoparsec.Text as AP
import Data.Char ( isSpace )
import Data.Maybe ( catMaybes )
import Data.Text ( Text, unpack, strip )
import qualified Data.Text as T ( empty )
import Numeric.Natural ( Natural )
import Prelude hiding ( lines )

----------------------------------------------------------------------------------------------------
-- Token -------------------------------------------------------------------------------------------

data Token
  = TknV Location
  | TknVN Normal
  | TknVT TexCoord
  | TknP [Point]
  | TknL [Line]
  | TknF Face
  | TknG [Text]
  | TknO Text
  | TknMtlLib [Text]
  | TknUseMtl Text
  | TknS Natural
    deriving (Eq,Show)

-- |A stream of 'Token'.
type TokenStream = [Token]

tokenize :: Text -> Either String TokenStream
tokenize = fmap cleanupTokens . analyseResult False . parse (untilEnd tokenizer)
  where
    tokenizer = choice
      [
        fmap (Just . TknV) location
      , fmap (Just . TknVN) normal
      , fmap (Just . TknVT) texCoord
      , fmap (Just . TknP) points
      , fmap (Just . TknL) lines
      , fmap (Just . TknF) face
      , fmap (Just . TknG) groups
      , fmap (Just . TknO) object
      , fmap (Just . TknMtlLib) mtllib
      , fmap (Just . TknUseMtl) usemtl
      , fmap (Just . TknS) smoothingGroup
      , Nothing <$ comment
      ]

analyseResult :: Bool -> Result [Maybe Token] -> Either String [Maybe Token]
analyseResult partial r = case r of
  Done _ tkns -> Right tkns
  Fail i _ e -> Left $ "`" ++ Prelude.take 10 (unpack i) ++ "` [...]: " ++ e
  Partial p -> if partial then Left "not completely tokenized" else analyseResult True (p T.empty)

cleanupTokens :: [Maybe Token] -> TokenStream
cleanupTokens = catMaybes

----------------------------------------------------------------------------------------------------
-- Location ----------------------------------------------------------------------------------------

location :: Parser Location
location = skipSpace *> string "v " *> skipHSpace *> parseXYZW <* eol
  where
    parseXYZW = do
      xyz <- float `sepBy1` skipHSpace
      case xyz of
        [x,y,z] -> pure (Location x y z 1)
        [x,y,z,w] -> pure (Location x y z w)
        _ -> fail "wrong number of x, y and z arguments for location"

----------------------------------------------------------------------------------------------------
-- Normal ------------------------------------------------------------------------------------------

normal :: Parser Normal
normal = skipSpace *> string "vn " *> skipHSpace *> parseIJK <* eol
  where
    parseIJK = do
      ijk <- float `sepBy1` skipHSpace
      case ijk of
        [i,j,k] -> pure (Normal i j k)
        _ -> fail "wrong number of i, j and k arguments for normal"

----------------------------------------------------------------------------------------------------
-- Texture coordinates -----------------------------------------------------------------------------

texCoord :: Parser TexCoord
texCoord = skipSpace *> string "vt " *> skipHSpace *> parseUVW <* eol
  where
    parseUVW = do
      uvw <- float `sepBy1` skipHSpace
      case uvw of
        [u,v] -> pure (TexCoord u v 0)
        [u,v,w] -> pure (TexCoord u v w)
        _ -> fail "wrong number of u, v and w arguments for texture coordinates"

----------------------------------------------------------------------------------------------------
-- Points ------------------------------------------------------------------------------------------

points :: Parser [Point]
points = skipSpace *> string "p " *> skipHSpace *> fmap Point decimal `sepBy1` skipHSpace <* eol

----------------------------------------------------------------------------------------------------
-- Lines -------------------------------------------------------------------------------------------
lines :: Parser [Line]
lines = do
    skipSpace
    _ <- string "l "
    skipHSpace
    pointIndices <- parsePointIndices
    pts <- case pointIndices of
      _:_:_ -> pure $ zipWith Line pointIndices (tail pointIndices)
      _ -> fail "line doesn't have at least two points"
    eol
    pure pts
  where
    parsePointIndices = fmap (\(i,j) -> LineIndex i j) parseLinePair `sepBy1` skipHSpace
    parseLinePair = do
      v <- decimal
      slashThenElse (fmap (\vt -> (v, Just vt)) decimal) (pure (v,Nothing))

----------------------------------------------------------------------------------------------------
-- Faces -------------------------------------------------------------------------------------------
face :: Parser Face
face = do
    skipSpace
    _ <- string "f "
    skipHSpace
    faceIndices <- parseFaceIndices
    f <- case faceIndices of
      a:b:c:s -> pure (Face a b c s)
      _ -> fail "face doesn't have at least three points"
    eol
    pure f
  where
    parseFaceIndices = fmap (\(i,k,j) -> FaceIndex i k j) parseFaceTriple `sepBy1` skipHSpace
    parseFaceTriple = do
      v <- decimal
      slashThenElse (parseVT v) (pure (v,Nothing,Nothing))
    parseVT v = slashThenElse (parseVN v Nothing) $ do
      vt <- decimal
      slashThenElse (parseVN v $ Just vt) (pure (v,Just vt,Nothing))
    parseVN v vt = do
      vn <- decimal
      pure (v,vt,Just vn)

----------------------------------------------------------------------------------------------------
-- Groups ------------------------------------------------------------------------------------------

groups :: Parser [Text]
groups = skipSpace *> string "g " *> skipHSpace *> name `sepBy` skipHSpace <* eol

----------------------------------------------------------------------------------------------------
-- Objects -----------------------------------------------------------------------------------------

object :: Parser Text
object = skipSpace *> string "o " *> skipHSpace *> spacedName <* eol

----------------------------------------------------------------------------------------------------
-- Material libraries ------------------------------------------------------------------------------

mtllib :: Parser [Text]
mtllib = skipSpace *> string "mtllib " *> skipHSpace *> name `sepBy1` skipHSpace <* eol

----------------------------------------------------------------------------------------------------
-- Using materials ---------------------------------------------------------------------------------

usemtl :: Parser Text
usemtl = skipSpace *> string "usemtl " *> skipHSpace *> spacedName <* eol

----------------------------------------------------------------------------------------------------
-- Smoothing groups --------------------------------------------------------------------------------
smoothingGroup :: Parser Natural
smoothingGroup = skipSpace *> string "s " *> skipHSpace *> offOrIndex <* skipHSpace <* eol
  where
    offOrIndex = string "off" *> pure 0 <|> decimal

----------------------------------------------------------------------------------------------------
-- Comments ----------------------------------------------------------------------------------------
comment :: Parser ()
comment = skipSpace *> string "#" *> (() <$ manyTill anyChar eol)

----------------------------------------------------------------------------------------------------
-- Special parsers ---------------------------------------------------------------------------------

-- Read a slash ('/') and run the @thenP@ parser on success. Otherwise, call the @elseP@ parser.
slashThenElse :: Parser a -> Parser a -> Parser a
slashThenElse thenP elseP = do
  c <- peekChar
  case c of
    Just '/' -> AP.take 1 *> thenP
    _ -> elseP

-- End of line.
eol :: Parser ()
eol = skipMany (satisfy isHorizontalSpace) *> (endOfLine <|> endOfInput)

-- Parse a name (any character but space).
name :: Parser Text
name = takeWhile1 $ not . isSpace

spacedName :: Parser Text
spacedName = strip <$> AP.takeWhile (flip notElem ("\n\r" :: String))

skipHSpace :: Parser ()
skipHSpace = () <$ AP.takeWhile isHorizontalSpace

float :: Parser Float
float = fmap realToFrac double

-- Loop a parser and collect its values until we hit the end of the stream. Fails on the first
-- failure.
untilEnd :: Parser a -> Parser [a]
untilEnd p = go
  where
    go = do
      a <- p
      end <- atEnd
      if end then pure [a] else fmap (a:) go