{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Data.Geometry.Ipe.Reader( 
                                 readRawIpeFile
                               , readIpeFile
                               , readSinglePageFile
                               , ConversionError
                               
                               , fromIpeXML
                               , readXML
                               
                               , IpeReadText(..)
                               , IpeRead(..)
                               , IpeReadAttr(..)
                               
                               , ipeReadTextWith
                               , ipeReadObject
                               , ipeReadAttrs
                               , ipeReadRec
                               , Coordinate(..)
                               ) where
import           Control.Applicative((<|>))
import           Control.Lens hiding (Const, rmap)
import qualified Data.ByteString as B
import           Data.Colour.SRGB (RGB(..))
import           Data.Either (rights)
import           Data.Ext
import           Data.Geometry.Box
import           Data.Geometry.Ipe.Attributes
import           Data.Geometry.Ipe.ParserPrimitives (pInteger, pWhiteSpace)
import           Data.Geometry.Ipe.PathParser
import           Data.Geometry.Ipe.Types
import           Data.Geometry.Ipe.Value
import           Data.Geometry.Ipe.Color(IpeColor(..))
import           Data.Geometry.Point
import           Data.Geometry.PolyLine
import qualified Data.Geometry.Polygon as Polygon
import qualified Data.Geometry.Transformation as Trans
import qualified Data.List as L
import qualified Data.List.NonEmpty as NE
import           Data.Maybe (fromMaybe, mapMaybe)
import           Data.Proxy
import qualified Data.LSeq as LSeq
import           Data.Singletons
import           Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Traversable as Tr
import           Data.Vinyl hiding (Label)
import           Data.Vinyl.Functor
import           Data.Vinyl.TypeLevel
import           Text.XML.Expat.Tree
type ConversionError = Text
readRawIpeFile :: (Coordinate r, Eq r)
               => FilePath -> IO (Either ConversionError (IpeFile r))
readRawIpeFile = fmap fromIpeXML . B.readFile
readIpeFile :: (Coordinate r, Eq r)
            => FilePath -> IO (Either ConversionError (IpeFile r))
readIpeFile = fmap (bimap id applyMatrices) . readRawIpeFile
readSinglePageFile :: (Coordinate r, Eq r)
                   => FilePath -> IO (Either ConversionError (IpePage r))
readSinglePageFile = fmap f . readIpeFile
  where
    f (Left e)  = Left e
    f (Right i) = maybe (Left "No Ipe pages found") Right . firstOf (pages.traverse) $ i
fromIpeXML   :: IpeRead (t r) => B.ByteString -> Either ConversionError (t r)
fromIpeXML b = readXML b >>= ipeRead
readXML :: B.ByteString -> Either ConversionError (Node Text Text)
readXML = bimap (T.pack . show) id . parse' defaultParseOptions
class IpeReadText t where
  ipeReadText :: Text -> Either ConversionError t
class IpeRead t where
  ipeRead  :: Node Text Text -> Either ConversionError t
instance IpeReadText Text where
  ipeReadText = Right
instance IpeReadText Int where
  ipeReadText = fmap fromInteger . runParser pInteger
instance Coordinate r => IpeReadText (Point 2 r) where
  ipeReadText = readPoint
instance Coordinate r => IpeReadText (Trans.Matrix 3 3 r) where
  ipeReadText = readMatrix
instance IpeReadText LayerName where
  ipeReadText = Right . LayerName
instance IpeReadText PinType where
  ipeReadText "yes" = Right Yes
  ipeReadText "h"   = Right Horizontal
  ipeReadText "v"   = Right Vertical
  ipeReadText ""    = Right No
  ipeReadText _     = Left "invalid PinType"
instance IpeReadText TransformationTypes where
  ipeReadText "affine"       = Right Affine
  ipeReadText "rigid"        = Right Rigid
  ipeReadText "translations" = Right Translations
  ipeReadText _              = Left "invalid TransformationType"
instance IpeReadText FillType where
  ipeReadText "wind"   = Right Wind
  ipeReadText "eofill" = Right EOFill
  ipeReadText _        = Left "invalid FillType"
instance Coordinate r => IpeReadText (IpeArrow r) where
  ipeReadText t = case T.split (== '/') t of
                    [n,s] -> IpeArrow <$> pure n <*> ipeReadText s
                    _     -> Left "ipeArrow: name contains not exactly 1 / "
instance Coordinate r => IpeReadText (IpeDash r) where
  ipeReadText t = Right . DashNamed $ t
                  
ipeReadTextWith     :: (Text -> Either t v) -> Text -> Either ConversionError (IpeValue v)
ipeReadTextWith f t = case f t of
                        Right v -> Right (Valued v)
                        Left _  -> Right (Named t)
instance Coordinate r => IpeReadText (Rectangle () r) where
  ipeReadText = readRectangle
instance Coordinate r => IpeReadText (RGB r) where
  ipeReadText = runParser (pRGB <|> pGrey)
    where
      pGrey = (\c -> RGB c c c) <$> pCoordinate
      pRGB  = RGB <$> pCoordinate <* pWhiteSpace
                  <*> pCoordinate <* pWhiteSpace
                  <*> pCoordinate
instance Coordinate r => IpeReadText (IpeColor r) where
  ipeReadText = fmap IpeColor . ipeReadTextWith ipeReadText
instance Coordinate r => IpeReadText (IpePen r) where
  ipeReadText = fmap IpePen . ipeReadTextWith readCoordinate
instance Coordinate r => IpeReadText (IpeSize r) where
  ipeReadText = fmap IpeSize . ipeReadTextWith readCoordinate
instance Coordinate r => IpeReadText [Operation r] where
  ipeReadText = readPathOperations
instance (Coordinate r, Eq r) => IpeReadText (NE.NonEmpty (PathSegment r)) where
  ipeReadText t = ipeReadText t >>= fromOpsN
    where
      fromOpsN xs = case fromOps xs of
                      Left l       -> Left l
                      Right []     -> Left "No path segments produced"
                      Right (p:ps) -> Right $ p NE.:| ps
      fromOps []            = Right []
      fromOps (MoveTo p:xs) = fromOps' p xs
      fromOps _             = Left "Path should start with a move to"
      fromOps' _ []             = Left "Found only a MoveTo operation"
      fromOps' s (LineTo q:ops) = let (ls,xs) = span' _LineTo ops
                                      pts  = map ext $ s:q:mapMaybe (^?_LineTo) ls
                                      poly = Polygon.fromPoints . dropRepeats $ pts
                                      pl   = fromPoints pts
                                  in case xs of
                                       (ClosePath : xs') -> PolygonPath poly   <<| xs'
                                       _                 -> PolyLineSegment pl <<| xs
      fromOps' _ _ = Left "fromOpts': rest not implemented yet."
      span' pr = L.span (not . isn't pr)
      x <<| xs = (x:) <$> fromOps xs
dropRepeats :: Eq a => [a] -> [a]
dropRepeats = map head . L.group
instance (Coordinate r, Eq r) => IpeReadText (Path r) where
  ipeReadText = fmap (Path . LSeq.fromNonEmpty) . ipeReadText
class IpeReadAttr t where
  ipeReadAttr  :: Text -> Node Text Text -> Either ConversionError t
instance IpeReadText (Apply f at) => IpeReadAttr (Attr f at) where
  ipeReadAttr n (Element _ ats _) = GAttr <$> Tr.mapM ipeReadText (lookup n ats)
  ipeReadAttr _ _                 = Left "IpeReadAttr: Element expected, Text found"
zipTraverseWith                       :: forall f g h i (rs :: [u]). Applicative h
                                      => (forall (x :: u). f x -> g x -> h (i x))
                                      -> Rec f rs -> Rec g rs -> h (Rec i rs)
zipTraverseWith _ RNil      RNil      = pure RNil
zipTraverseWith f (x :& xs) (y :& ys) = (:&) <$> f x y <*> zipTraverseWith f xs ys
ipeReadRec       :: forall f ats.
                 ( RecApplicative ats
                 , ReifyConstraint IpeReadAttr (Attr f) ats
                 , RecAll (Attr f) ats IpeReadAttr
                 , AllSatisfy IpeAttrName ats
                 )
                 => Proxy f -> Proxy ats
                 -> Node Text Text
                 -> Either ConversionError (Rec (Attr  f) ats)
ipeReadRec _ _ x = zipTraverseWith f (writeAttrNames r) r'
  where
    r  = rpure (GAttr Nothing)
    r' = reifyConstraint @IpeReadAttr r
    f                              :: forall at.
                                      Const Text at
                                   -> (Dict IpeReadAttr :. Attr f) at
                                   -> Either ConversionError (Attr f at)
    f (Const n) (Compose (Dict _)) = ipeReadAttr n x
ipeReadAttrs     :: forall proxy proxy' i r f ats.
                 ( f ~ AttrMapSym1 r, ats ~ AttributesOf i
                 , ReifyConstraint IpeReadAttr (Attr f) ats
                 , RecApplicative ats
                 , RecAll (Attr f) ats IpeReadAttr
                 , AllSatisfy IpeAttrName ats
                 )
                 => proxy i -> proxy' r
                 -> Node Text Text
                 -> Either ConversionError (IpeAttributes i r)
ipeReadAttrs _ _ = fmap Attrs . ipeReadRec (Proxy :: Proxy f) (Proxy :: Proxy ats)
ipeReadObject           :: ( IpeRead (i r)
                           , f ~ AttrMapSym1 r, ats ~ AttributesOf i
                           , RecApplicative ats
                           , ReifyConstraint IpeReadAttr (Attr f) ats
                           , RecAll (Attr f) ats IpeReadAttr
                           , AllSatisfy IpeAttrName ats
                           )
                        => Proxy i -> proxy r -> Node Text Text
                        -> Either ConversionError (i r :+ IpeAttributes i r)
ipeReadObject prI prR xml = (:+) <$> ipeRead xml <*> ipeReadAttrs prI prR xml
instance Coordinate r => IpeRead (IpeSymbol r) where
  ipeRead (Element "use" ats _) = case lookup "pos" ats of
      Nothing -> Left "symbol without position"
      Just ps -> flip Symbol name <$> ipeReadText ps
    where
      name = fromMaybe "mark/disk(sx)" $ lookup "name" ats
  ipeRead _ = Left "symbol element expected, text found"
allText :: [Node Text Text] -> Either ConversionError Text
allText = fmap T.unlines . mapM unT
  where
    unT (Text t) = Right t
    unT _        = Left "allText: Expected Text, found an Element"
instance (Coordinate r, Eq r) => IpeRead (Path r) where
  ipeRead (Element "path" _ chs) = allText chs >>= ipeReadText
  ipeRead _                      = Left "path: expected element, found text"
lookup'   :: Text -> [(Text,a)] -> Either ConversionError a
lookup' k = maybe (Left $ "lookup' " <> k <> " not found") Right . lookup k
instance Coordinate r => IpeRead (TextLabel r) where
  ipeRead (Element "text" ats chs)
    | lookup "type" ats == Just "label" = Label
                                       <$> allText chs
                                       <*> (lookup' "pos" ats >>= ipeReadText)
    | otherwise                         = Left "Not a Text label"
  ipeRead _                             = Left "textlabel: Expected element, found text"
instance Coordinate r => IpeRead (MiniPage r) where
  ipeRead (Element "text" ats chs)
    | lookup "type" ats == Just "minipage" = MiniPage
                                          <$> allText chs
                                          <*> (lookup' "pos"   ats >>= ipeReadText)
                                          <*> (lookup' "width" ats >>= readCoordinate)
    | otherwise                            = Left "Not a MiniPage"
  ipeRead _                                = Left "MiniPage: Expected element, found text"
instance Coordinate r => IpeRead (Image r) where
  ipeRead (Element "image" ats _) = Image () <$> (lookup' "rect" ats >>= ipeReadText)
  ipeRead _                       = Left "Image: Element expected, text found"
instance (Coordinate r, Eq r) => IpeRead (IpeObject r) where
  ipeRead x = firstRight [ IpeUse       <$> ipeReadObject (Proxy :: Proxy IpeSymbol) r x
                         , IpePath      <$> ipeReadObject (Proxy :: Proxy Path)      r x
                         , IpeGroup     <$> ipeReadObject (Proxy :: Proxy Group)     r x
                         , IpeTextLabel <$> ipeReadObject (Proxy :: Proxy TextLabel) r x
                         , IpeMiniPage  <$> ipeReadObject (Proxy :: Proxy MiniPage)  r x
                         , IpeImage     <$> ipeReadObject (Proxy :: Proxy Image)     r x
                         ]
    where
      r = Proxy :: Proxy r
firstRight :: [Either ConversionError a] -> Either ConversionError a
firstRight = maybe (Left "No matching object") Right . firstOf (traverse._Right)
instance (Coordinate r, Eq r) => IpeRead (Group r) where
  ipeRead (Element "group" _ chs) = Right . Group . rights . map ipeRead $ chs
  ipeRead _                       = Left "ipeRead Group: expected Element, found Text"
instance IpeRead LayerName where
  ipeRead (Element "layer" ats _) = LayerName <$> lookup' "name" ats
  ipeRead _                       = Left "layer: Expected element, found text"
instance IpeRead View where
  ipeRead (Element "view" ats _) = (\lrs a -> View (map LayerName $ T.words lrs) a)
                                <$> lookup' "layers" ats
                                <*> (lookup' "active" ats >>= ipeReadText)
  ipeRead _                      = Left "View Expected element, found text"
instance (Coordinate r, Eq r) => IpeRead (IpePage r) where
  ipeRead (Element "page" _ chs) = Right $ IpePage (readAll chs) (readAll chs) (readAll chs)
  ipeRead _                      = Left "page: Element expected, text found"
      
      
      
      
      
readAll   :: IpeRead a => [Node Text Text] -> [a]
readAll   = rights . map ipeRead
instance (Coordinate r, Eq r) => IpeRead (IpeFile r) where
  ipeRead (Element "ipe" _ chs) = case readAll chs of
                                    []  -> Left "Ipe: no pages found"
                                    pgs -> Right $ IpeFile Nothing [] (NE.fromList pgs)
  ipeRead _                     = Left "Ipe: Element expected, text found"