{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Data.Geometry.Ipe.IpeOut where

import           Control.Lens hiding (Simple)
import           Data.Bifunctor
import           Data.Ext
import           Data.Geometry.Ball
import           Data.Geometry.Boundary
import           Data.Geometry.Box
import           Data.Geometry.Ipe.Attributes
import           Data.Geometry.Ipe.FromIpe
import           Data.Geometry.Ipe.Types
import           Data.Geometry.Line
import           Data.Geometry.LineSegment
import           Data.Geometry.Point
import           Data.Geometry.PolyLine
import           Data.Geometry.Polygon
import           Data.Geometry.Polygon.Convex
import           Data.Geometry.Properties
import           Data.Geometry.Transformation
import           Data.Maybe (fromMaybe)
import           Data.Proxy
import           Data.Semigroup
import qualified Data.Seq2 as S2
import           Data.Text (Text)
import           Data.Vinyl.CoRec

--------------------------------------------------------------------------------

-- | An IpeOut is essentially a funciton to convert a geometry object of type
-- 'g' into an ipe object of type 'i'.
newtype IpeOut g i = IpeOut { asIpe :: g -> i } deriving (Functor)


-- | Given an geometry object, and a record with its attributes, construct an ipe
-- Object representing it using the default conversion.
asIpeObject :: (HasDefaultIpeOut g, DefaultIpeOut g ~ i, NumType g ~ r)
            => g -> IpeAttributes i r -> IpeObject r
asIpeObject = asIpeObjectWith defaultIpeOut

-- | asIpeObject with its arguments flipped. Convenient if you don't want to
-- map asIpeObject over a list or so.
asIpeObject' :: (HasDefaultIpeOut g, DefaultIpeOut g ~ i, NumType g ~ r)
             => IpeAttributes i r -> g -> IpeObject r
asIpeObject' = flip asIpeObject


-- -- | Given a IpeOut that specifies how to convert a geometry object into an
-- ipe geometry object, the geometry object, and a record with its attributes,
-- construct an ipe Object representing it.
asIpeObjectWith          :: (ToObject i, NumType g ~ r)
                         => IpeOut g (IpeObject' i r) -> g -> IpeAttributes i r
                         -> IpeObject r
asIpeObjectWith io g ats = asIpe (ipeObject io ats) g


-- | Create an ipe group without group attributes
asIpeGroup :: [IpeObject r] -> IpeObject r
asIpeGroup = flip asIpeGroup' mempty

-- | Creates a group out of ipe
asIpeGroup'        :: [IpeObject r] -> IpeAttributes Group r -> IpeObject r
asIpeGroup' gs ats = IpeGroup $ Group gs :+ ats

--------------------------------------------------------------------------------

-- | Helper to construct an IpeOut g IpeObject , if we already know how to
-- construct a specific Ipe type.
ipeObject        :: (ToObject i, NumType g ~ r)
                   => IpeOut g (IpeObject' i r) -> IpeAttributes i r -> IpeOut g (IpeObject r)
ipeObject io ats = IpeOut $ \g -> let (i :+ ats') = asIpe io g
                                    in ipeObject' i (ats' <> ats)

-- | Construct an ipe object from the core of an Ext
coreOut    :: IpeOut g i -> IpeOut (g :+ a) i
coreOut io = IpeOut $ asIpe io . (^.core)

--------------------------------------------------------------------------------
-- * Default Conversions

-- | Class that specifies a default conversion from a geometry type g into an
-- ipe object.
class ToObject (DefaultIpeOut g) => HasDefaultIpeOut g where
  type DefaultIpeOut g :: * -> *

  defaultIpeOut :: IpeOut g (IpeObject' (DefaultIpeOut g) (NumType g))

  -- defaultIpeObject :: RecApplicative (AttributesOf (DefaultIpeOut g))
  --                  => IpeOut g (IpeObject (NumType g))
  -- defaultIpeObject = IpeOut $ flip asIpeObject mempty

-- instance HasDefaultIpeOut g => HasDefaultIpeOut [g] where
--   type DefaultIpeOut [g] = Group
--   defaultIpeOut = IpeOut $ asIpeGroup . map (asIpeObject' mempty)

instance HasDefaultIpeOut (Point 2 r) where
  type DefaultIpeOut (Point 2 r) = IpeSymbol
  defaultIpeOut = ipeDiskMark

instance HasDefaultIpeOut (LineSegment 2 p r) where
  type DefaultIpeOut (LineSegment 2 p r) = Path
  defaultIpeOut = ipeLineSegment

instance Floating r => HasDefaultIpeOut (Disk p r) where
  type DefaultIpeOut (Disk p r) = Path
  defaultIpeOut = ipeDisk

instance HasDefaultIpeOut (PolyLine 2 p r) where
  type DefaultIpeOut (PolyLine 2 p r) = Path
  defaultIpeOut = noAttrs ipePolyLine

instance HasDefaultIpeOut (Polygon t p r) where
  type DefaultIpeOut (Polygon t p r) = Path
  defaultIpeOut = flip addAttributes ipePolygon $
                    mempty <> attr SFill (IpeColor "0.722 0.145 0.137")

instance HasDefaultIpeOut (SomePolygon p r) where
  type DefaultIpeOut (SomePolygon p r) = Path
  defaultIpeOut = IpeOut $ either (asIpe defaultIpeOut) (asIpe defaultIpeOut)

instance HasDefaultIpeOut (ConvexPolygon p r) where
  type DefaultIpeOut (ConvexPolygon p r) = Path
  defaultIpeOut = IpeOut $ asIpe defaultIpeOut . view simplePolygon

--------------------------------------------------------------------------------
-- * Point Converters

ipeMark   :: Text -> IpeOut (Point 2 r) (IpeObject' IpeSymbol r)
ipeMark n = noAttrs . IpeOut $ flip Symbol n

ipeDiskMark :: IpeOut (Point 2 r) (IpeObject' IpeSymbol r)
ipeDiskMark = ipeMark "mark/disk(sx)"

--------------------------------------------------------------------------------

noAttrs :: Monoid extra => IpeOut g core -> IpeOut g (core :+ extra)
noAttrs = addAttributes mempty

addAttributes :: extra -> IpeOut g core -> IpeOut g (core :+ extra)
addAttributes ats io = IpeOut $ \g -> asIpe io g :+ ats


-- | Default size of the cliping rectangle used to clip lines. This is
-- Rectangle is large enough to cover the normal page size in ipe.
defaultClipRectangle :: (Num r, Ord r) => Rectangle () r
defaultClipRectangle = boundingBox (point2 (-200) (-200)) <>
                       boundingBox (point2 1000 1000)

-- | An ipe out to draw a line, by clipping it to stay within a rectangle of
-- default size.
line :: (Fractional r, Ord r) => IpeOut (Line 2 r) (IpeObject' Path r)
line = lineWith defaultClipRectangle

-- | An ipe out to draw a line, by clipping it to stay within the rectangle.
--
-- pre: intersection of the line and the rectangle is a line segment
-- (otherwise it arbitrarily inserts the bottom of the rectangle as the path)
lineWith   :: forall p r. (Ord r, Fractional r)
              => Rectangle p r -> IpeOut (Line 2 r) (IpeObject' Path r)
lineWith r = IpeOut (asIpe defaultIpeOut . clip)
  where
    def    = bimap (const ()) id $ bottomSide r
    clip l = fromMaybe def . asA (Proxy :: Proxy (LineSegment 2 () r))
           $ l `intersect` r

ipeLineSegment :: IpeOut (LineSegment 2 p r) (IpeObject' Path r)
ipeLineSegment = noAttrs $ fromPathSegment ipeLineSegment'

ipeLineSegment' :: IpeOut (LineSegment 2 p r) (PathSegment r)
ipeLineSegment' = IpeOut $ PolyLineSegment . fromLineSegment . first (const ())


ipePolyLine :: IpeOut (PolyLine 2 p r) (Path r)
ipePolyLine = fromPathSegment ipePolyLine'

ipePolyLine' :: IpeOut (PolyLine 2 a r) (PathSegment r)
ipePolyLine' = IpeOut $ PolyLineSegment . first (const ())

ipeDisk :: Floating r => IpeOut (Disk p r) (IpeObject' Path r)
ipeDisk = noAttrs . IpeOut $ asIpe ipeCircle . Boundary

ipeCircle :: Floating r => IpeOut (Circle p r) (Path r)
ipeCircle = fromPathSegment ipeCircle'

ipeCircle' :: Floating r => IpeOut (Circle p r) (PathSegment r)
ipeCircle' = IpeOut circle''
  where
    circle'' (Circle (c :+ _) r) = EllipseSegment m
      where
        m = translation (toVec c) |.| uniformScaling (sqrt r) ^. transformationMatrix
        -- m is the matrix s.t. if we apply m to the unit circle centered at the origin, we
        -- get the input circle.


-- | Helper to construct a IpeOut g Path, for when we already have an IpeOut g PathSegment
fromPathSegment    :: IpeOut g (PathSegment r) -> IpeOut g (Path r)
fromPathSegment io = IpeOut $ Path . S2.l1Singleton . asIpe io


ipePolygon :: IpeOut (Polygon t p r) (Path r)
ipePolygon = IpeOut $ io . first (const ())
  where
    io                       :: forall t r. Polygon t () r -> Path r
    io pg@(SimplePolygon _)  = pg^.re _asSimplePolygon
    io pg@(MultiPolygon _ _) = pg^.re _asMultiPolygon









-- ls = (ClosedLineSegment (ext origin) (ext (point2 1 1)))


-- testzz :: IpeObject Integer
-- testzz = asIpeObjectWith ipeLineSegment ls $ mempty <> attr SStroke (IpeColor "red")




-- test' :: Attributes (PathAttrElfSym1 Integer) (AttributesOf (Path Integer) (PathAttrElfSym1 Integer))
-- -- test' :: RecApplicative (AttributesOf (Path Integer) (IpeObjectSymbolF (Path Integer)))
-- --       => IpeAttributes (Path Integer)
-- test' = mempty




-- -- test' :: IpeObject Integer ('IpePath '[])
-- test' = asIpeObject' ls emptyPathAttributes




-- emptyPathAttributes :: Rec (PathAttribute r) '[]
-- emptyPathAttributes = RNil