packages feed

penrose-0.1.1.0: src/Shapes.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveGeneric   #-}
{-# LANGUAGE RankNTypes      #-}
{-# OPTIONS_HADDOCK prune #-}

module Shapes where

import           Data.Aeson      (FromJSON, ToJSON, toJSON)
import           Data.List       (foldl')
import qualified Data.Map.Strict as M
import           Data.Maybe      (fromMaybe)
import           Debug.Trace
import           GHC.Generics
import           System.Random
import           Transforms
import           Utils

default (Int, Float)

-- import Language.Haskell.TH
--
-- type Autofloat a = (RealFloat a, Floating a, Real a, Show a, Ord a)
-- type Pt2 a = (a, a)
-- genShapeType :: [ShapeTypeStr] -> Q [Dec]
-- genShapeType shapeTypes = do
--     let mkconstructor n = NormalC (mkName n) []
--         constructors    = map mkconstructor shapeTypes
--     return [DataD [] (mkName "ShapeT") [] Nothing constructors []]
--------------------------------------------------------------------------------
-- Types
-- | shape can have multiple pieces, e.g. multiple "M"s
type PathData a = [Path' a]

-- | TODO
-- NOTE: the order of the elements is important
data Path' a
  = Closed [Elem a] -- ^  "Z"
  | Open [Elem a] -- ^  no "Z"
  deriving (Generic, Eq, Show)

instance (FromJSON a) => FromJSON (Path' a)

instance (ToJSON a) => ToJSON (Path' a)

-- | TODO
-- | Arc { x, y, sweep1, … }  -- "A" TODO
data Elem a
  = Pt (Pt2 a) -- ^ Replace "M," "L", "H", "V"
  | CubicBez (Pt2 a, Pt2 a, Pt2 a) -- ^ "C": two control pts, 1 endpt
  | CubicBezJoin (Pt2 a, Pt2 a) -- ^ "S": 1 control pt, 1 endpt
  | QuadBez (Pt2 a, Pt2 a) -- ^ "Q": 1 control pt, 1 endpt
  | QuadBezJoin (Pt2 a) -- ^ "T": 1 endpt
  deriving (Generic, Eq, Show)

instance (FromJSON a) => FromJSON (Elem a)

instance (ToJSON a) => ToJSON (Elem a)

-------------- Types
-- | possible values in the argument of computation, constraint, or objectives
-- Used for type checking functions
data ArgVal a
  = GPI (Shape a)
  | Val (Value a)
  deriving (Eq, Show, Generic)

data ArgType
  = GPIType ShapeTypeStr
  | ValueT ValueType
  | OneOf [ShapeTypeStr]
  | AnyGPI
  deriving (Eq, Show)

-- | types of fully evaluated values in Style
data ValueType
  = FloatT
  | IntT
  | BoolT
  | StrT
  | PtT
  | PtListT
  | ListT
  | TupT
  | LListT
  | PathDataT
  | ColorT
  | FileT
  | StyleT
  | TransformT
  | HMatrixT
  | MapT
  | PolygonT
  deriving (Eq, Show)

-- | fully evaluated values in Style
data Value a
  = FloatV a -- ^ floating point number
  | IntV Integer -- ^ integer
  | BoolV Bool -- ^ boolean value
  | StrV String -- ^ string literal
  | PtV (Pt2 a) -- ^ point in R^2
  | PathDataV (PathData a) -- ^ path commands
  | PtListV [Pt2 a] -- ^ a list of points
  | ColorV Color -- ^ an RGBA color value
  | FileV String -- ^ path for image
  | StyleV String -- ^ dotted, etc.
  | ListV [a] -- ^ a list of floats
  | TupV (a, a) -- ^ a tuple of floats
  | LListV [[a]] -- ^ a 2D list of floats
  | HMatrixV (HMatrix a) -- ^ single transformation (homogeneous transformation)
  | PolygonV (Polygon a) -- ^ multiple shapes with holes
  deriving (Generic, Eq, Show)

instance (FromJSON a) => FromJSON (Value a)

instance (ToJSON a) => ToJSON (Value a)

instance (FromJSON a) => FromJSON (ArgVal a)

instance (ToJSON a) => ToJSON (ArgVal a)

-- | returns the type of a 'Value'
typeOf :: (Autofloat a) => Value a -> ValueType
typeOf v =
  case v of
    FloatV _    -> FloatT
    IntV _      -> IntT
    BoolV _     -> BoolT
    StrV _      -> StrT
    PtV _       -> PtT
    PtListV _   -> PtListT
    TupV _      -> TupT
    ListV _     -> ListT
    LListV _    -> LListT
    PathDataV _ -> PathDataT
    ColorV _    -> ColorT
    FileV _     -> FileT
    StyleV _    -> StyleT
    HMatrixV _  -> HMatrixT
    PolygonV _  -> PolygonT

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

toPolymorphics :: [Shape Double] -> (forall a . (Autofloat a) => [Shape a])
toPolymorphics = map toPolymorphic

toPolymorphic :: Shape Double -> (forall a . (Autofloat a) => Shape a)
toPolymorphic (ctor, properties) = (ctor, M.map toPolyProperty properties)

toPolyArgVal :: ArgVal Double -> (forall a . (Autofloat a) => ArgVal a)
toPolyArgVal (GPI x) = GPI $ toPolymorphic x
toPolyArgVal (Val x) = Val $ toPolyProperty x

toPolyProperty :: Value Double -> (forall a . (Autofloat a) => Value a)
toPolyProperty v = case v of
    -- Not sure why these have to be rewritten from scratch...
    FloatV n  -> FloatV $ r2f n
    BoolV x   -> BoolV x
    StrV x    -> StrV x
    IntV x    -> IntV x
    PtV p -> PtV $ r2 p
    PtListV xs  -> PtListV $ map r2 xs
    ListV xs -> ListV $ map r2f xs
    TupV x -> TupV $ r2 x
    LListV xs -> LListV $ map (map r2f) xs
    ColorV x  -> ColorV x
    FileV x   -> FileV x
    StyleV x  -> StyleV x
    PathDataV es -> PathDataV $ map toPolyPath es
    HMatrixV m -> HMatrixV $ HMatrix { xScale = r2f $ xScale m,
                                       xSkew = r2f $ xSkew m,
                                       ySkew = r2f $ ySkew m,
                                       yScale = r2f $ yScale m,
                                       dx = r2f $ dx m,
                                       dy = r2f $ dy m
                                     }
    PolygonV (b, h, bbox, samples) -> PolygonV $ (
        map (map r2) b, map (map r2) h, map2 r2 bbox, map r2 samples)
    where r2 (x, y) = (r2f x, r2f y)

toPolyPath :: Path' Double -> (forall a . (Autofloat a) => Path' a)
toPolyPath (Closed es) = Closed $ map toPolyElem es
toPolyPath (Open es) = Open $ map toPolyElem es

toPolyElem :: Elem Double -> (forall a . (Autofloat a) => Elem a)
toPolyElem (Pt p) = Pt $ r2ft p
toPolyElem (CubicBez (p0, p1, p2)) = CubicBez (r2ft p0, r2ft p1, r2ft p2)
toPolyElem (CubicBezJoin (p0, p1)) = CubicBezJoin (r2ft p0, r2ft p1)
toPolyElem (QuadBez (p0, p1)) = QuadBez (r2ft p0, r2ft p1)
toPolyElem (QuadBezJoin p) = QuadBezJoin $ r2ft p
r2ft (x, y) = (r2f x, r2f y)

-- | the type string of a shape
type ShapeTypeStr = String
-- | the string identifier of a property
type PropID = String

--------- new
type PropertiesDef2 a = M.Map PropID (ValueType, SampledValue a)
-- | Mutually recursive types because we might want to nest a definition of properties (e.g. A.shape.center.x)
type PropertyValue a = Either (SampledValue a) (PropertiesDef2 a)

-- | A dict storing names, types, and default values of properties
type PropertiesDef1 a = M.Map PropID (ValueType, PropertyValue a)
type ShapeDef' a = (ShapeTypeStr, PropertiesDef1 a)
----------

-- | A dict storing names, types, and default values of properties
type PropertiesDef a = M.Map PropID (ValueType, SampledValue a)

-- | definition of a new shape/graphical primitive
-- TODO: rewrite as a record?
type ShapeDef a = (ShapeTypeStr, PropertiesDef a)

type ShapeDefs a = M.Map ShapeTypeStr (ShapeDef a)

-- | A dictionary storing properties of a Style object, e.g. "startx" for 'Arrow'
-- COMBAK: serializer to JSON
type Properties a = M.Map PropID (Value a)

-- | definition of a new shape/graphical primitive
-- TODO: rewrite as a record? Probably better for serialization
type Shape a = (ShapeTypeStr, Properties a)

--------------------------------------------------------------------------------
-- Shape introspection functions

-- | all of the shape defs supported in the system
shapeDefs :: (Autofloat a) => ShapeDefs a
shapeDefs = M.fromList $ zipWithKey shapeDefList
    where zipWithKey = map (\x -> (fst x, x))

shapeDefList :: (Autofloat a) => [ShapeDef a]

shapeDefList = [ anchorPointType, circType, ellipseType, arrowType, braceType, curveType, lineType, rectType, squareType, parallelogramType, imageType, textType, arcType, rectTransformType, polygonType, circTransformType, curveTransformType, lineTransformType, squareTransformType, imageTransformType, ellipseTransformType, parallelogramTransformType, textTransformType ]

-- | retrieve type strings of all shapes
shapeTypes :: (Autofloat a) => ShapeDefs a -> [ShapeTypeStr]
shapeTypes defs = map fst $ M.toList defs

-- | given a type string, find the corresponding shape def
findDef :: (Autofloat a) => ShapeTypeStr -> ShapeDef a
findDef typ = fromMaybe
    (noShapeError "findDef" typ)
    (M.lookup typ shapeDefs)

-- | given a shape def, construct a default shape
defaultShapeOf :: (Autofloat a) => StdGen -> ShapeDef a -> (Shape a, StdGen)
defaultShapeOf g (t, propDict) =
    let (properties, g') = sampleProperties g propDict in
    ((t, properties), g')

defaultValueOf :: (Autofloat a) => StdGen -> PropID -> ShapeDef a -> (Value a, StdGen)
defaultValueOf g prop (t, propDict) =
    let sampleF = snd $ fromMaybe
            (noPropError "defaultValueOf" prop t)
            (M.lookup prop propDict)
    in sampleF g

--------------------------------------------------------------------------------
-- Computing derived properties

-- Initially, a computed property starts with the initial value specified in the shape initialized (e.g. idH)

-- To say that a property is computed: add it to the list in `computedProperties`
-- to say how it is computed: specify the input properties of the shape and write a function that expects them and returns a value
-- Note: DOFs are preserved through this

-- How this feature is implemented: see `computeProperty` in GenOptProblem, which is called by `evalExpr` (as well as anything that calls `evalExpr`, like `evalShape` and `initShape`
-- evalExpr, for a PropertyPath X.f.p, checks if p is in computedProperties for the type of X.f.
-- If so, it evaluates the GPI properties requested by the function, passes the values to the function, and returns the value the function returns
-- If not, then it evaluates the path as usual (by looking up its expr and evaluating it)
-- TODO: move implementation documentation to the wiki and PR

type ComputedValue a = ([Property], [Value a] -> Value a)

computedProperties :: (Autofloat a) => M.Map (ShapeTypeStr, Property) (ComputedValue a)
computedProperties = M.fromList [(("RectangleTransform", "transformation"), rectTransformFn),
                                 (("CircleTransform", "transformation"), circTransformFn),
                                 (("Polygon", "transformation"), polygonTransformFn),
                                 (("CurveTransform", "transformation"), polygonTransformFn),
                                                     -- Same parameters as polygon
                                 (("LineTransform", "transformation"), polygonTransformFn),
                                                    -- Same parameters as polygon
                                 (("SquareTransform", "transformation"), squareTransformFn),
                                 (("ImageTransform", "transformation"), imageTransformFn),
                                 (("EllipseTransform", "transformation"), ellipseTransformFn),
                                 (("ParallelogramTransform", "transformation"), parallelogramTransformFn),
                                 (("TextTransform", "transformation"), textTransformFn),
                                 (("RectangleTransform", "polygon"), rectPolygonFn),
                                 (("CircleTransform", "polygon"), circPolygonFn),
                                 (("CurveTransform", "polygon"), curvePolygonFn),
                                 (("Polygon", "polygon"), polygonPolygonFn),
                                 (("LineTransform", "polygon"), linePolygonFn),
                                 (("SquareTransform", "polygon"), squarePolygonFn),
                                 (("ImageTransform", "polygon"), imagePolygonFn),
                                 (("EllipseTransform", "polygon"), ellipsePolygonFn),
                                 (("ParallelogramTransform", "polygon"), parallelogramPolygonFn),
                                 (("TextTransform", "polygon"), textPolygonFn)
                                ]

rectTransformFn :: (Autofloat a) => ComputedValue a
rectTransformFn = (props, fn)
    where props = ["sizeX", "sizeY", "rotation", "x", "y", "transform"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV sizeX, FloatV sizeY, FloatV rotation, 
              FloatV x, FloatV y, HMatrixV customTransform] = 
             let defaultTransform = paramsToMatrix (sizeX, sizeY, rotation, x, y) in
             HMatrixV $ customTransform # defaultTransform

polygonTransformFn :: (Autofloat a) => ComputedValue a
polygonTransformFn = (props, fn)
    where props = ["scaleX", "scaleY", "rotation", "dx", "dy", "transform"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV scaleX, FloatV scaleY, FloatV rotation, 
              FloatV dx, FloatV dy, HMatrixV customTransform] = 
             let defaultTransform = paramsToMatrix (scaleX, scaleY, rotation, dx, dy) in
             HMatrixV $ customTransform # defaultTransform

circTransformFn :: (Autofloat a) => ComputedValue a
circTransformFn = (props, fn)
    where props = ["x", "y", "r", "transform"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV x, FloatV y, FloatV r, HMatrixV customTransform] = 
             let defaultTransform = paramsToMatrix (r, r, 0.0, x, y) in
             HMatrixV $ customTransform # defaultTransform

squareTransformFn :: (Autofloat a) => ComputedValue a
squareTransformFn = (props, fn)
    where props = ["x", "y", "side", "rotation", "transform"]
          fn :: (Autofloat a ) => [Value a] -> Value a
          fn [FloatV x, FloatV y, FloatV side, FloatV rotation, HMatrixV customTransform] =
             let defaultTransform = paramsToMatrix (side, side, rotation, x, y) in
             HMatrixV $ customTransform # defaultTransform

imageTransformFn :: Autofloat a => ComputedValue a
imageTransformFn = (props, fn)
    where props = ["centerX", "centerY", "scaleX", "scaleY", "rotation", "transform"]
          fn :: (Autofloat a ) => [Value a] -> Value a
          fn [FloatV centerX, FloatV centerY, FloatV scaleX, FloatV scaleY, FloatV rotation, HMatrixV customTransform] =
             let defaultTransform = paramsToMatrix (scaleX, scaleY, rotation, centerX, centerY) in
             HMatrixV $ customTransform # defaultTransform

ellipseTransformFn :: Autofloat a => ComputedValue a
ellipseTransformFn = (props, fn)
    where props = ["x", "y", "rx", "ry", "rotation", "transform"]
          fn :: Autofloat a => [Value a] -> Value a
          fn [FloatV x, FloatV y, FloatV rx, FloatV ry, FloatV rotation, HMatrixV customTransform] = 
             let defaultTransform = paramsToMatrix (rx, ry, rotation, x, y) in
             HMatrixV $ customTransform # defaultTransform

textTransformFn :: Autofloat a => ComputedValue a
textTransformFn = (props, fn)
    where props = ["x", "y", "scaleX", "scaleY", "rotation", "transform"]
          fn :: (Autofloat a ) => [Value a] -> Value a
          fn [FloatV x, FloatV y, FloatV scaleX, FloatV scaleY, FloatV rotation, HMatrixV customTransform] =
             -- Note that this overall transformation does NOT use the "w" and "h" parameters set by the frontend
             let defaultTransform = paramsToMatrix (scaleX, scaleY, rotation, x, y) in
             HMatrixV $ customTransform # defaultTransform

parallelogramTransformFn :: (Autofloat a) => ComputedValue a
parallelogramTransformFn = (props, fn)
    where props = ["width", "height", "rotation", "x", "y", "innerAngle", "transform"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV width, FloatV height, FloatV rotation, 
              FloatV x, FloatV y, FloatV innerAngle, HMatrixV customTransform] = 
             let defaultTransform = toParallelogram (width, height, rotation, x, y, innerAngle) in
             HMatrixV $ customTransform # defaultTransform

-- TODO: there's a bit of redundant computation with recomputing the full transformation
rectPolygonFn :: (Autofloat a) => ComputedValue a
rectPolygonFn = (props, fn)
    where props = ["sizeX", "sizeY", "rotation", "x", "y", "transform"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV sizeX, FloatV sizeY, FloatV rotation, 
              FloatV x, FloatV y, HMatrixV customTransform] = 
             let defaultTransform = paramsToMatrix (sizeX, sizeY, rotation, x, y) in
             let fullTransform = customTransform # defaultTransform in
             PolygonV $ transformPoly fullTransform $ toPoly unitSq

polygonPolygonFn :: (Autofloat a) => ComputedValue a
polygonPolygonFn = (props, fn)
    where props = ["scaleX", "scaleY", "rotation", "dx", "dy", "transform", "points"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV scaleX, FloatV scaleY, FloatV rotation, 
              FloatV dx, FloatV dy, HMatrixV customTransform, PolygonV points] = 
             let defaultTransform = paramsToMatrix (scaleX, scaleY, rotation, dx, dy) in
             let fullTransform = customTransform # defaultTransform in
             PolygonV $ transformPoly fullTransform points

circPolygonFn :: (Autofloat a) => ComputedValue a
circPolygonFn = (props, fn)
    where props = ["x", "y", "r", "transform"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV x, FloatV y, FloatV r, HMatrixV customTransform] = 
             let defaultTransform = paramsToMatrix (r, r, 0.0, x, y) in
             let fullTransform = customTransform # defaultTransform in
             let res = PolygonV $ transformPoly fullTransform $ toPoly $ circlePoly r in
             {-trace ("getting circle polygon output: " ++ show res)-} res

-- | Polygonize a Bezier curve, even if the curve was originally made using a list of points.
-- TODO: distinguish between filled curves (polygons) and unfilled ones (polylines)
curvePolygonFn :: (Autofloat a) => ComputedValue a
curvePolygonFn = (props, fn)
    where props = ["scaleX", "scaleY", "rotation", "dx", "dy", "transform", "pathData", "strokeWidth",  "left-arrowhead", "right-arrowhead"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV scaleX, FloatV scaleY, FloatV rotation, 
              FloatV dx, FloatV dy, HMatrixV customTransform, PathDataV path, 
              FloatV strokeWidth, BoolV leftArrow, BoolV rightArrow] = 
             let defaultTransform = paramsToMatrix (scaleX, scaleY, rotation, dx, dy) in
             let fullTransform = customTransform # defaultTransform in
             PolygonV $ transformPoly fullTransform $ toPoly $ polygonizePathPolygon maxIter strokeWidth leftArrow rightArrow path
             where maxIter = 1 -- TODO: what should this be?

-- | Polygonize a line segment, accounting for its thickness. 
-- TODO: would it usually be more efficient to just use a polyline?
linePolygonFn :: (Autofloat a) => ComputedValue a
linePolygonFn = (props, fn)
    where props = ["scaleX", "scaleY", "rotation", "dx", "dy", "transform", "thickness", "startX", "startY", "endX", "endY", "left-arrowhead", "right-arrowhead"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV scaleX, FloatV scaleY, FloatV rotation, 
              FloatV dx, FloatV dy, HMatrixV customTransform,
              FloatV thickness, FloatV startX, FloatV startY, FloatV endX, FloatV endY,
              BoolV leftArrow, BoolV rightArrow] = 
             let defaultTransform = paramsToMatrix (scaleX, scaleY, rotation, dx, dy) in
             let fullTransform = customTransform # defaultTransform in
             PolygonV $ transformPoly fullTransform $ toPoly $ extrude thickness (startX, startY) (endX, endY) leftArrow rightArrow

-- TODO: add ones for final properties; also refactor so it's more generic across shapes
squarePolygonFn :: (Autofloat a) => ComputedValue a
squarePolygonFn = (props, fn)
    where props = ["x", "y", "side", "rotation", "transform"]
          fn :: (Autofloat a ) => [Value a] -> Value a
          fn [FloatV x, FloatV y, FloatV side, FloatV rotation, HMatrixV customTransform] = let
              defaultTransform = paramsToMatrix (side, side, rotation, x, y)
              fullTransform = customTransform # defaultTransform
              in PolygonV $ transformPoly fullTransform $ toPoly unitSq

imagePolygonFn :: Autofloat a => ComputedValue a
imagePolygonFn = (props, fn)
    where props = ["centerX", "centerY", "scaleX", "scaleY", "rotation", "transform", "initWidth", "initHeight"]
          fn :: (Autofloat a ) => [Value a] -> Value a
          fn [FloatV centerX, FloatV centerY, FloatV scaleX, FloatV scaleY, FloatV rotation, HMatrixV customTransform, FloatV initWidth, FloatV initHeight] = let
             -- Note that the unit square is implicitly scaled to (w, h)
             -- (from the frontend) before having the default transform applied
             defaultTransform = paramsToMatrix (scaleX * initWidth, scaleY * initHeight, rotation, centerX, centerY)
             fullTransform = customTransform # defaultTransform 
             in PolygonV $ transformPoly fullTransform $ toPoly unitSq

ellipsePolygonFn :: Autofloat a => ComputedValue a
ellipsePolygonFn = (props, fn)
    where props = ["x", "y", "rx", "ry", "rotation", "transform"]
          fn :: Autofloat a => [Value a] -> Value a
          fn [FloatV x, FloatV y, FloatV rx, FloatV ry, FloatV rotation, HMatrixV customTransform] = let
             defaultTransform = paramsToMatrix (rx, ry, rotation, x, y)
             fullTransform = customTransform # defaultTransform 
             in PolygonV $ transformPoly fullTransform $ toPoly $ circlePoly 1

parallelogramPolygonFn :: (Autofloat a) => ComputedValue a
parallelogramPolygonFn = (props, fn)
    where props = ["width", "height", "rotation", "x", "y", "innerAngle", "transform"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV width, FloatV height, FloatV rotation, 
              FloatV x, FloatV y, FloatV innerAngle, HMatrixV customTransform] = 
             let defaultTransform = toParallelogram (width, height, rotation, x, y, innerAngle) in
             let fullTransform = customTransform # defaultTransform in
             PolygonV $ transformPoly fullTransform $ toPoly unitSq

textPolygonFn :: (Autofloat a) => ComputedValue a
textPolygonFn = (props, fn)
    where props = ["scaleX", "scaleY", "rotation", "x", "y", "transform", "w", "h"]
          fn :: (Autofloat a) => [Value a] -> Value a
          fn [FloatV scaleX, FloatV scaleY, FloatV rotation, 
              FloatV x, FloatV y, HMatrixV customTransform, FloatV w, FloatV h] = 
             -- Note that the unit square is implicitly scaled to (w, h)
             -- (from the frontend) before having the default transform applied
             let defaultTransform = paramsToMatrix (scaleX * w, scaleY * h, rotation, x, y) in
             let fullTransform = customTransform # defaultTransform in
             PolygonV $ transformPoly fullTransform $ toPoly unitSq

--------------------------------------------------------------------------------
-- Property samplers

type SampledValue a = StdGen -> (Value a, StdGen)
type FloatInterval = (Float, Float)

rndInterval :: (Float, Float)
rndInterval = (0, canvasWidth / 6)

-- COMBAK: SHAME. Parametrize the random generators properly!
canvasHeight, canvasWidth :: Float
canvasHeight = 700.0
canvasWidth  = 800.0

debugRng :: StdGen
debugRng = mkStdGen seed
    where seed = 16 -- deterministic RNG with seed

constValue :: (Autofloat a) => Value a -> SampledValue a
constValue v g = (v, g)

-- NOTE: this function does not enforce that all values have the same type
sampleDiscrete :: (Autofloat a) => [Value a] -> SampledValue a
sampleDiscrete list g =
    let (idx, g') = randomR (0, length list - 1) g
    in (list !! idx, g')

sampleFloatIn :: (Autofloat a) => FloatInterval -> SampledValue a
sampleFloatIn interval g =
    let (n, g') = randomR interval g in (FloatV $ r2f n, g')

samplePointIn :: (Autofloat a) =>
    (FloatInterval, FloatInterval)  -> SampledValue a
samplePointIn (interval1, interval2) g =
    let (n1, g1) = randomR interval1 g
        (n2, g2) = randomR interval2 g1
    in (PtV (r2f n1, r2f n2), g2)

sampleColor :: (Autofloat a) => SampledValue a
sampleColor rng =
    let interval = (0.1, 0.9)
        (r, rng1)  = randomR interval rng
        (g, rng2)  = randomR interval rng1
        (b, rng3)  = randomR interval rng2
    in (ColorV $ makeColor r g b 0.5, rng3)
        -- (a, rng4)  = randomR (0.3, 0.7) rng3
    -- in (ColorV $ makeColor r g b a, rng4)

-- | Samples all properties of input shapes
sampleShapes :: (Autofloat a) => StdGen -> [Shape a] -> ([Shape a], StdGen)
sampleShapes g shapes =
    let (shapes', g') = foldl' sampleShape ([], g) shapes
    in (reverse shapes', g')
sampleShape (shapes, g) oldShape@(typ, oldProperties) =
    let (_, propDefs)    = findDef typ
        (properties, g') = sampleProperties g propDefs
        shape            = (typ, properties)
        namedShape       = setName (getName oldShape) shape
    in (namedShape : shapes, g')

sampleProperties :: (Autofloat a) => StdGen -> PropertiesDef a -> (Properties a, StdGen)
sampleProperties g propDefs = M.foldlWithKey sampleProperty (M.empty, g) propDefs

sampleProperty :: (Autofloat a) => (Properties a, StdGen) -> PropID -> (ValueType, SampledValue a) -> (Properties a, StdGen)
sampleProperty (properties, g) propID (typ, sampleF) =
    let (val, g') = sampleF g in (M.insert propID val properties, g')

--------------------------------------------------------------------------------
-- Example shape defs

-- | TODO: derived properties
-- | TODO: instantiation of objs with (1) default values; (2) random sampling w.r.t. constraints
-- constructShape :: ShapeDef a -> [SampleRule] -> Shape a

canvasDims :: (Float, Float)
canvasDims = (-canvasHeight / 2, canvasHeight / 2)

-- TODO: change everything to camel case
x_sampler, y_sampler, pointSampler, width_sampler, height_sampler, angle_sampler, stroke_sampler, stroke_style_sampler, bool_sampler :: (Autofloat a) => SampledValue a
x_sampler = sampleFloatIn canvasDims
y_sampler = sampleFloatIn canvasDims
pointSampler = samplePointIn (canvasDims, canvasDims)
width_sampler = sampleFloatIn (3, canvasWidth / 6)
height_sampler = sampleFloatIn (3, canvasHeight / 6)
angle_sampler = sampleFloatIn (0, 360) -- TODO: check that frontend uses degrees, not radians
stroke_sampler = sampleFloatIn (0.5, 3)
stroke_style_sampler = sampleDiscrete [StrV "dashed", StrV "solid"]
bool_sampler = sampleDiscrete [BoolV True, BoolV False]

anchorPointType, circType, ellipseType, arrowType, braceType, curveType, lineType, rectType, squareType, parallelogramType, imageType, textType, arcType, rectTransformType, polygonType, circTransformType, curveTransformType, lineTransformType, squareTransformType, imageTransformType, ellipseTransformType, parallelogramTransformType, textTransformType :: (Autofloat a) => ShapeDef a

anchorPointType = ("AnchorPoint", M.fromList
    [
        -- ("x", (FloatT, x_sampler)),
        -- ("y", (FloatT, y_sampler)),
        ("location", (PtT, pointSampler)),
        ("name", (StrT, constValue $ StrV "defaultAnchorPoint"))
    ])

circType = ("Circle", M.fromList
    [
        ("x", (FloatT, x_sampler)),
        ("y", (FloatT, y_sampler)),
        ("r", (FloatT, width_sampler)),
        ("strokeWidth", (FloatT, stroke_sampler)),
        ("style", (StrT, constValue $ StrV "filled")),
        ("strokeStyle", (StrT, constValue $ StrV "solid")),
        ("strokeColor", (ColorT, sampleColor)),
        ("color", (ColorT, sampleColor)),
        ("name", (StrT, constValue $ StrV "defaultCircle"))
    ])

ellipseType = ("Ellipse", M.fromList
    [
        ("x", (FloatT, x_sampler)),
        ("y", (FloatT, y_sampler)),
        ("rx", (FloatT, width_sampler)),
        ("ry", (FloatT, height_sampler)),
        ("rotation", (FloatT, constValue $ FloatV 0.0)),
        ("stroke-width", (FloatT, stroke_sampler)),
        ("style", (StrT, sampleDiscrete [StrV "filled"])),
        ("stroke-style", (StrT, stroke_style_sampler)),
        ("color", (ColorT, sampleColor)),
        ("name", (StrT, constValue $ StrV "defaultEllipse"))
    ])

textType = ("Text", M.fromList
    [
        ("x", (FloatT, sampleFloatIn (-canvasWidth / 2, canvasWidth / 2))),
        ("y", (FloatT, sampleFloatIn (-canvasHeight / 2, canvasHeight / 2))),
        ("w", (FloatT, constValue $ FloatV 0)), -- NOTE: updated by front-end
        ("h", (FloatT, constValue $ FloatV 0)), -- NOTE: updated by front-end
        ("string", (StrT, constValue $ StrV "defaultLabelText")),
        ("rotation", (FloatT, constValue $ FloatV 0.0)),
        ("style", (StrT, constValue $ StrV "none")),
        ("stroke", (StrT, constValue $ StrV "none")),
        ("color", (ColorT, constValue $ ColorV black)),
        ("name", (StrT, constValue $ StrV "defaultCircle"))
    ])

arrowType = ("Arrow", M.fromList
    [
        ("startX", (FloatT, x_sampler)),
        ("startY", (FloatT, y_sampler)),
        ("endX", (FloatT, x_sampler)),
        ("endY", (FloatT, y_sampler)),
        ("thickness", (FloatT, sampleFloatIn (5, 15))),
        ("style", (StrT, constValue $ StrV "straight")),
        ("color", (ColorT, sampleColor)),
        ("name", (StrT, constValue $ StrV "defaultArrow")),
        ("rotation", (FloatT, constValue $ FloatV 0.0))
    ])

braceType = ("Brace", M.fromList
    [
        ("startX", (FloatT, x_sampler)),
        ("startY", (FloatT, y_sampler)),
        ("endX", (FloatT, x_sampler)),
        ("endY", (FloatT, y_sampler)),
        ("color", (ColorT, sampleColor)),
        ("thickness", (FloatT, sampleFloatIn (1, 6))),
        ("name", (StrT, constValue $ StrV "defaultBrace"))
      ])

curveType = ("Curve", M.fromList
    [
        -- These two fields are for storage.
        ("path", (PtListT, constValue $ PtListV [])), -- TODO: sample path
        ("polyline", (PtListT, constValue $ PtListV [])), -- TODO: sample path
        -- The frontend only uses pathData to draw the curve.
        ("pathData", (PathDataT, constValue $ PathDataV [])), -- TODO: sample path
        ("strokeWidth", (FloatT, stroke_sampler)),
        ("style", (StrT, constValue $ StrV "solid")),
        ("fill", (ColorT, sampleColor)), -- for no fill, set opacity to 0
        ("color", (ColorT, sampleColor)),
        ("left-arrowhead", (BoolT, constValue $ BoolV False)),
        ("right-arrowhead", (BoolT, constValue $ BoolV False)),
        ("name", (StrT, constValue $ StrV "defaultCurve"))
    ])

lineType = ("Line", M.fromList
    [
        ("startX", (FloatT, x_sampler)),
        ("startY", (FloatT, y_sampler)),
        ("endX", (FloatT, x_sampler)),
        ("endY", (FloatT, y_sampler)),
        ("thickness", (FloatT, sampleFloatIn (5, 15))),

        ("left-arrowhead", (BoolT, constValue $ BoolV False)),
        ("right-arrowhead", (BoolT, constValue $ BoolV False)),
        ("color", (ColorT, sampleColor)),
        ("style", (StrT, constValue $ StrV "solid")),
        ("stroke", (StrT, constValue $ StrV "none")),
        ("name", (StrT, constValue $ StrV "defaultLine"))
    ])

rectType = ("Rectangle", M.fromList
    [
        ("x", (FloatT, x_sampler)),
        ("y", (FloatT, y_sampler)),
        ("sizeX", (FloatT, width_sampler)),
        ("sizeY", (FloatT, height_sampler)),
        ("rotation", (FloatT, constValue $ FloatV 0.0)),
        ("color", (ColorT, sampleColor)),
        ("strokeWidth", (FloatT, stroke_sampler)),
        ("style", (StrT, constValue $ StrV "filled")),
        ("strokeColor", (ColorT, sampleColor)),
        ("strokeStyle", (StrT, constValue $ StrV "none")),
        ("name", (StrT, constValue $ StrV "defaultRect"))
    ])

squareType = ("Square", M.fromList
    [
        ("x", (FloatT, x_sampler)),
        ("y", (FloatT, y_sampler)),
        ("side", (FloatT, width_sampler)),
        ("rotation", (FloatT, constValue $ FloatV 0.0)),
        -- TODO: distinguish between stroke color and fill color everywhere
        ("color", (ColorT, sampleColor)),
        ("style", (StrT, constValue $ StrV "none")), -- TODO: what is this?
        ("strokeColor", (ColorT, sampleColor)),
        ("strokeWidth", (FloatT, constValue $ FloatV 0.0)),
        ("name", (StrT, constValue $ StrV "defaultSquare"))
    ])

parallelogramType = ("Parallelogram", M.fromList
    [
        ("x", (FloatT, x_sampler)), -- (x, y) is the bottom-left corner of the parallelogram
        ("y", (FloatT, y_sampler)),
        ("lengthX", (FloatT, width_sampler)),
        ("lengthY", (FloatT, height_sampler)),
        ("angle", (FloatT, constValue $ FloatV 0.0)),
        ("rotation", (FloatT, constValue $ FloatV 0.0)),
        ("color", (ColorT, sampleColor)),
        ("stroke-style", (StrT, stroke_style_sampler)),
        ("stroke-color",  (ColorT, sampleColor)),
        ("name", (StrT, constValue $ StrV "defaultParallelogram"))
    ])

imageType = ("Image", M.fromList
    [
        ("centerX", (FloatT, x_sampler)),
        ("centerY", (FloatT, y_sampler)),
        ("lengthX", (FloatT, width_sampler)),
        ("lengthY", (FloatT, height_sampler)),
        ("rotation", (FloatT, constValue $ FloatV 0.0)),
        ("style", (StrT, constValue $ StrV "none")),
        ("stroke", (StrT, constValue $ StrV "none")),
        ("path", (StrT, constValue $ StrV "missing image path")), -- Absolute path (URL)
        ("name", (StrT, constValue $ StrV "defaultImage"))
    ])

arcType = ("Arc", M.fromList
    [
        ("x", (FloatT, x_sampler)), -- x,y are the cordinates for the bottom left position
        ("y", (FloatT, y_sampler)), -- of the right angle or the midlle pos of a regular angle
        ("r", (FloatT, width_sampler)),
        ("size", (FloatT, width_sampler)),
        ("lengthX", (FloatT, width_sampler)),
        ("lengthY", (FloatT, height_sampler)),
        ("angle", (FloatT, angle_sampler)),
        ("rotation", (FloatT, constValue $ FloatV 0.0)),

        ("isRight", (BoolT, bool_sampler)), -- This property overrides the angle property

        ("strokeWidth", (FloatT, constValue $ FloatV 1.0)),
        ("strokeColor", (ColorT, sampleColor)),
        ("fillColor", (ColorT, sampleColor)),
        ("left-arrowhead", (BoolT, constValue $ BoolV False)),
        ("right-arrowhead", (BoolT, constValue $ BoolV False)),

        ("name", (StrT, constValue $ StrV "defaultArc"))
    ])

rectTransformType = ("RectangleTransform", M.fromList
    [
        -- These attributes serve as DOF in the default transformation
        -- They are NOT the final x, etc.
        ("x", (FloatT, x_sampler)),
        ("y", (FloatT, y_sampler)),
        ("sizeX", (FloatT, width_sampler)),
        ("sizeY", (FloatT, height_sampler)),
        ("rotation", (FloatT, angle_sampler)),

        ("transform", (FloatT, constValue $ HMatrixV idH)), -- Set in Style
        ("transformation", (FloatT, constValue $ HMatrixV idH)), -- Computed

        -- TODO compute these
        -- ("finalX", (FloatT, constValue $ FloatV 0.0)),
        -- ("finalY", (FloatT, constValue $ FloatV 0.0)),
        -- ("finalSizeX", (FloatT, constValue $ FloatV 1.0)),
        -- ("finalSizeY", (FloatT, constValue $ FloatV 1.0)),
        -- ("finalRotation", (FloatT, constValue $ FloatV 0.0)),

        ("color", (ColorT, sampleColor)),
        ("strokeWidth", (FloatT, stroke_sampler)),
        ("style", (StrT, constValue $ StrV "filled")),
        ("strokeColor", (ColorT, sampleColor)),
        ("strokeStyle", (StrT, constValue $ StrV "none")),
        ("name", (StrT, constValue $ StrV "defaultRect")),
        ("polygon", (PolygonT, constValue $ PolygonV emptyPoly))
    ])

-- Also using the new transforms
polygonType = ("Polygon", M.fromList
    [
        ("points", (PtListT, constValue $ PtListV testTriangle)),
        ("polygon", (PtListT, constValue $ PtListV [])),

        -- These attributes serve as DOF in the default transformation
        -- They are NOT the final x, etc.
        -- TODO: should these be sampled?
        ("dx", (FloatT, x_sampler)), -- Polygon doesn't have a natural "center"
        ("dy", (FloatT, y_sampler)),
        ("scaleX", (FloatT, width_sampler)),
        ("scaleY", (FloatT, height_sampler)),
        ("rotation", (FloatT, angle_sampler)),
        -- TODO: currently rotates about the center of the Penrose canvas, (0, 0)

        ("centerX", (FloatT, constValue $ FloatV 0.0)),
        ("centerY", (FloatT, constValue $ FloatV 0.0)),

        ("transform", (FloatT, constValue $ HMatrixV idH)),
        ("transformation", (FloatT, constValue $ HMatrixV idH)), -- Computed

        ("strokeWidth", (FloatT, stroke_sampler)),
        ("strokeStyle", (StrT, constValue $ StrV "solid")),
        ("strokeColor", (ColorT, sampleColor)),

        ("fillColor", (ColorT, sampleColor)),
        ("name", (StrT, constValue $ StrV "defaultPolygon"))
    ])

circTransformType = ("CircleTransform", M.fromList
    [
        ("x", (FloatT, x_sampler)),
        ("y", (FloatT, y_sampler)),
        ("r", (FloatT, width_sampler)),
        -- TODO: circle currently has rotations hardcoded to 0.0 (since we aren't yet rotating about a point)

        ("transform", (FloatT, constValue $ HMatrixV idH)),
        ("transformation", (FloatT, constValue $ HMatrixV idH)), -- Computed
        ("polygon", (PolygonT, constValue $ PolygonV emptyPoly)),

        ("strokeWidth", (FloatT, stroke_sampler)),
        ("style", (StrT, constValue $ StrV "filled")),
        ("strokeStyle", (StrT, constValue $ StrV "solid")),
        ("strokeColor", (ColorT, sampleColor)),
        ("color", (ColorT, sampleColor)),
        ("name", (StrT, constValue $ StrV "defaultCircle"))
    ])

curveTransformType = ("CurveTransform", M.fromList
    [
        ("dx", (FloatT, constValue $ FloatV 0.0)), -- Curve doesn't have a natural "center"
        ("dy", (FloatT, constValue $ FloatV 0.0)),
        ("scaleX", (FloatT, constValue $ FloatV 1.0)),
        ("scaleY", (FloatT, constValue $ FloatV 1.0)),
        ("rotation", (FloatT, constValue $ FloatV 0.0)),
        -- TODO: currently rotates about the center of the Penrose canvas, (0, 0)

        ("transform", (FloatT, constValue $ HMatrixV idH)),
        ("transformation", (FloatT, constValue $ HMatrixV idH)), -- Computed
        ("polygon", (PolygonT, constValue $ PolygonV emptyPoly)),
    
        ("path", (PtListT, constValue $ PtListV [])), -- TODO: sample path
        ("polyline", (PtListT, constValue $ PtListV [])), -- TODO: sample path
        ("pathData", (PathDataT, constValue $ PathDataV [])), -- TODO: sample path
        ("strokeWidth", (FloatT, stroke_sampler)),
        ("style", (StrT, constValue $ StrV "solid")),
        ("fill", (ColorT, sampleColor)), -- for no fill, set opacity to 0
        ("color", (ColorT, sampleColor)),
        ("left-arrowhead", (BoolT, constValue $ BoolV False)),
        ("right-arrowhead", (BoolT, constValue $ BoolV False)),
        ("name", (StrT, constValue $ StrV "defaultCurve"))
    ])

-- If the start and end are not set, by default, it's a unit line segment aligned w/ x-axis, centered at origin.
-- If the start and end are set in Style, then that will be the line segment to which all subsequent transforms apply.
lineTransformType = ("LineTransform", M.fromList
    [
        ("startX", (FloatT, constValue $ FloatV $ -0.5)),
        ("startY", (FloatT, constValue $ FloatV 0)),
        ("endX", (FloatT, constValue $ FloatV 0.5)),
        ("endY", (FloatT, constValue $ FloatV 0.0)),

        -- By default, this is NOT set. Should it be?
        ("dx", (FloatT, constValue $ FloatV 0.0)), -- Curve doesn't have a natural "center"
        ("dy", (FloatT, constValue $ FloatV 0.0)),
        ("scaleX", (FloatT, constValue $ FloatV 1.0)),
        ("scaleY", (FloatT, constValue $ FloatV 1.0)),
        ("rotation", (FloatT, constValue $ FloatV 0.0)),
        -- TODO: currently rotates about the center of the Penrose canvas, (0, 0)

        ("transform", (FloatT, constValue $ HMatrixV idH)),
        ("transformation", (FloatT, constValue $ HMatrixV idH)), -- Computed
        ("polygon", (PolygonT, constValue $ PolygonV emptyPoly)),

        ("left-arrowhead", (BoolT, constValue $ BoolV False)),
        ("right-arrowhead", (BoolT, constValue $ BoolV False)),

        ("thickness", (FloatT, sampleFloatIn (5, 15))),
        ("color", (ColorT, sampleColor)),
        ("style", (StrT, constValue $ StrV "solid")),
        ("stroke", (StrT, constValue $ StrV "none")),
        ("name", (StrT, constValue $ StrV "defaultLine"))
    ])

squareTransformType = ("SquareTransform", M.fromList
    [
        ("x", (FloatT, x_sampler)), -- x and y as dx and dy in transform?
        ("y", (FloatT, y_sampler)),
        ("side", (FloatT, width_sampler)), -- as both scaleX and scaleY ? 
        ("rotation", (FloatT, constValue $ FloatV 0.0)), -- initialize as 0?
        -- matrices
        ("transform", (FloatT, constValue $ HMatrixV idH)),
        ("transformation", (FloatT, constValue $ HMatrixV idH)),
        -- ones that remain the same
        ("color", (ColorT, sampleColor)),
        ("style", (StrT, constValue $ StrV "none")),
        ("strokeColor", (ColorT, sampleColor)),
        ("strokeWidth", (FloatT, constValue $ FloatV 0.0)),
        ("name", (StrT, constValue $ StrV "defaultSquare")),
        ("polygon", (PolygonT, constValue $ PolygonV emptyPoly))
    ])

imageTransformType = ("ImageTransform", M.fromList
    [
        ("centerX", (FloatT, x_sampler)),
        ("centerY", (FloatT, y_sampler)),

        ("initWidth", (FloatT, constValue $ FloatV 0.0)), -- Set by frontend
        ("initHeight", (FloatT, constValue $ FloatV 0.0)), -- (same)

        ("scaleX", (FloatT, constValue $ FloatV 1.0)), -- set by image file?
        ("scaleY", (FloatT, constValue $ FloatV 1.0)), 
        ("rotation", (FloatT, constValue $ FloatV 0.0)),
        ("transform", (FloatT, constValue $ HMatrixV idH)),
        ("transformation", (FloatT, constValue $ HMatrixV idH)),

        ("style", (StrT, constValue $ StrV "none")),
        ("stroke", (StrT, constValue $ StrV "none")),
        ("path", (StrT, constValue $ StrV "missing image path")), -- Absolute path (URL)
        ("name", (StrT, constValue $ StrV "defaultImage")),
        ("polygon", (PolygonT, constValue $ PolygonV emptyPoly))
    ])

ellipseTransformType = ("EllipseTransform", M.fromList
    [
        ("x", (FloatT, x_sampler)), --dx
        ("y", (FloatT, y_sampler)), --dy
        ("rx", (FloatT, width_sampler)), --sx
        ("ry", (FloatT, height_sampler)), --sy
        ("rotation", (FloatT, constValue $ FloatV 0.0)), --rot
        ("transform", (FloatT, constValue $ HMatrixV idH)),
        ("transformation", (FloatT, constValue $ HMatrixV idH)),
        ("stroke-width", (FloatT, stroke_sampler)),
        ("style", (StrT, sampleDiscrete [StrV "filled"])),
        ("stroke-style", (StrT, stroke_style_sampler)),
        ("color", (ColorT, sampleColor)),
        ("name", (StrT, constValue $ StrV "defaultEllipse")),
        ("polygon", (PolygonT, constValue $ PolygonV emptyPoly))
    ])

-- Starting with a square centered at origin, we apply this transformation:
--  (in T1 then T2 format)
-- scale by (lengthX, lengthY)
-- shear X by lambda = f(angle in radians) = 1 / tan(angle)
-- rotate by angle
-- translate by (x, y)
parallelogramTransformType = ("ParallelogramTransform", M.fromList
    [
        ("x", (FloatT, x_sampler)), -- (x, y) is the CENTER of the parallelogram
        ("y", (FloatT, y_sampler)),
        ("width", (FloatT, width_sampler)), -- width of the rectangle pre-shear
        ("height", (FloatT, height_sampler)), -- height of the rectangle pre-shear
        ("innerAngle", (FloatT, constValue $ FloatV (pi/3))), -- shear angle of the rectangle
        ("rotation", (FloatT, constValue $ FloatV 0.0)), -- about the origin

        ("transform", (FloatT, constValue $ HMatrixV idH)),
        ("transformation", (FloatT, constValue $ HMatrixV idH)), -- Computed
        ("polygon", (PolygonT, constValue $ PolygonV emptyPoly)),
    
        ("color", (ColorT, sampleColor)),
        ("strokeStyle", (StrT, stroke_style_sampler)),
        ("strokeColor",  (ColorT, sampleColor)),
        ("strokeWidth",  (FloatT, constValue $ FloatV 0.0)),
        ("name", (StrT, constValue $ StrV "defaultParallelogram"))
    ])

textTransformType = ("TextTransform", M.fromList
    [
        ("x", (FloatT, x_sampler)),
        ("y", (FloatT, y_sampler)),
        ("scaleX", (FloatT, constValue $ FloatV 1.0)), -- TODO: set text size in points
        ("scaleY", (FloatT, constValue $ FloatV 1.0)),
        ("rotation", (FloatT, constValue $ FloatV 0.0)),

        ("w", (FloatT, constValue $ FloatV 0)), -- NOTE: updated by front-end
        ("h", (FloatT, constValue $ FloatV 0)), -- NOTE: updated by front-end
        ("path", (PathDataT, constValue $ PathDataV [])), -- NOTE: updated by front-end

        -- NOTE: the polygon will only show up after a few steps, since we have to wait for the server to set the width/height/path
        ("transform", (FloatT, constValue $ HMatrixV idH)),
        ("transformation", (FloatT, constValue $ HMatrixV idH)), -- Computed
        ("polygon", (PolygonT, constValue $ PolygonV emptyPoly)),

        ("string", (StrT, constValue $ StrV "defaultLabelTextTransform")),
        ("style", (StrT, constValue $ StrV "none")),
        ("stroke", (StrT, constValue $ StrV "none")),
        ("color", (ColorT, constValue $ ColorV black)),
        ("name", (StrT, constValue $ StrV "defaultCircle"))
    ])

-----

exampleCirc :: (Autofloat a) => Shape a
exampleCirc = ("Circle", M.fromList
    [
        ("x", FloatV 5.5),
        ("y", FloatV 100.2),
        ("r", FloatV 5),
        ("name", StrV "exampleCirc"),
        ("style", StyleV "filled"),
        ("color", ColorV black)
    ])

--------------------------------------------------------------------------------
-- Parser for shape def DSL (TODO)

--------------------------------------------------------------------------------
-- Type checker for a particular shape instance against its def (TODO)
--
-- checkShape :: (Autofloat a) => Shape a -> ShapeDef a -> Shape a
-- checkShape shape def =

--------------------------------------------------------------------------------
-- Utility functions for Runtime

-- | given a translation generated by the Style compiler, generate all GPIs
-- NOTE: equilavant to genAllObjs
-- generateShapes :: (Autofloat a) => Translation a -> [Shape a]
-- COMBAK:
-- - where to define default objs such as sizeFuncs?
-- - do we allow extended properties? If so, where do we resolve them?
-- generateShapes trans = []
    -- TODO write out full procedure

findShape :: (Autofloat a) => String -> [Shape a] -> Shape a
findShape shapeName shapes =
    case filter (\s -> getName s == shapeName) shapes of
        [x] -> x
        _   -> error ("findShape: expected one shape for \"" ++ shapeName ++ "\", but did not find just one (returned zero or many).")

-- TODO: can use alter, update, adjust here. Come back if performance matters
-- | Setting the value of a property
set :: (Autofloat a) => Shape a -> PropID -> Value a -> Shape a
set (t, propDict) prop val = case M.lookup prop propDict of
    Nothing -> noPropError "set" prop t
    _       -> (t, M.update (const $ Just val) prop propDict)

-- | Getting the value of a property
get :: (Autofloat a) => Shape a -> PropID -> Value a
get (t, propDict) prop = fromMaybe
    (noPropError "get" prop t)
    (M.lookup prop propDict)
{-# INLINE get #-}

-- | batch get
getAll :: (Autofloat a) => Shape a -> [PropID] -> [Value a]
getAll shape = map (get shape)

-- | batch set
setAll :: (Autofloat a) => Shape a -> [(PropID, Value a)] -> Shape a
setAll = foldl' (\s (k, v) -> set s k v)


-- | reset a property to its default value
-- TODO: now needs to take in a random generator
-- reset :: (Autofloat a) => Shape a -> PropID -> Shape a
-- reset (t, propDict) prop =
--     let val = defaultValueOf prop $ findDef t shapeDefs
--     in (t, M.update (const $ Just val) prop propDict)

-- | whether a shape has a prop
hasProperty :: (Autofloat a) => Shape a -> PropID -> Bool
hasProperty (t, propDict) prop = M.member prop propDict

-- | given name of prop, return type
typeOfProperty :: (Autofloat a) => PropID -> Shape a -> ValueType
typeOfProperty prop (t, propDict) = case M.lookup prop propDict of
    Nothing -> noPropError "typeOfProperty" prop t
    Just v  -> typeOf v

-- | property IDs in alphabetical order
propertyIDs :: (Autofloat a) => Shape a -> [PropID]
propertyIDs (_, propDict) = map fst $ M.toAscList propDict

-- | vals in alphabetical order of their keys
propertyVals :: (Autofloat a) => Shape a -> [Value a]
propertyVals (_, propDict) = map snd $ M.toAscList propDict

--------------------------------------------------------------------------------
-- Utility functions for objective/constraint function writers

-- | 'is' checks whether a shape is of a certain type
is :: (Autofloat a) => Shape a -> ShapeTypeStr -> Bool
is (t1, _) t2 = t1 == t2

-- | short-hand for 'get'
(.:) :: (Autofloat a) => Shape a -> PropID -> Value a
(.:) = get
{-# INLINE (.:) #-}

getX, getY :: (Autofloat a) => Shape a -> a
getX shape = case shape .: "x" of
    FloatV x -> x
    _ -> error "getX: expected float but got something else"
getY shape = case shape .: "y" of
    FloatV y -> y
    _ -> error "getY: expected float but got something else"

getPoint :: (Autofloat a) => String -> Shape a -> (a, a)
getPoint "start" shape = case (shape .: "startX", shape .: "startY") of
   (FloatV x, FloatV y) -> (x, y)
   _ -> error "getPoint expected two floats but got something else"
getPoint "end" shape = case (shape .: "endX", shape .: "endY") of
   (FloatV x, FloatV y) -> (x, y)
   _ -> error "getPoint expected two floats but got something else"
getPoint _ shape = error "getPoint did not receive existing property name"

-- To use with vector operations
getPointV :: (Autofloat a) => String -> Shape a -> [a]
getPointV "start" shape = case (shape .: "startX", shape .: "startY") of
   (FloatV x, FloatV y) -> [x, y]
   _ -> error "getPointV expected two floats but got something else"
getPointV "end" shape = case (shape .: "endX", shape .: "endY") of
   (FloatV x, FloatV y) -> [x, y]
   _ -> error "getPointV expected two floats but got something else"
getPointV _ shape = error "getPointV did not receive existing property name"

getName :: (Autofloat a) => Shape a -> String
getName shape = case shape .: "name" of
    StrV s -> s
    _ -> error "getName: expected string but got something else"

setName :: (Autofloat a) => String -> Shape a -> Shape a
setName v shape = set shape "name" (StrV v)

setX, setY :: (Autofloat a) => Value a -> Shape a -> Shape a
setX v shape@("Arrow", _) = set shape "startX" v
setX v shape = set shape "x" v

setY v shape@("Arrow", _) = set shape "startY" v
setY v shape = set shape "y" v

getNum :: (Autofloat a) => Shape a -> PropID -> a
getNum shape prop = case shape .: prop of
    FloatV x -> x
    res -> error ("getNum: expected float but got something else: " ++ show res)

getPoints :: (Autofloat a) => Shape a -> [Pt2 a]
getPoints shape = case shape .: "points" of
    PtListV x -> x
    _ -> error "getPoints: expected [(Float, Float)] but got something else"

getPolygon :: (Autofloat a) => Shape a -> Polygon a
getPolygon shape = case shape .: "polygon" of
    PolygonV x -> x
    _ -> error "getPolygon: expected [(Float, Float)] but got something else"

getPath :: (Autofloat a) => Shape a -> [Pt2 a]
getPath shape = case shape .: "path" of
    PtListV x -> x
    _ -> error "getPath: expected [(Float, Float)] but got something else"

getPathData :: (Autofloat a) => Shape a -> PathData a
getPathData shape = case shape .: "pathData" of
    PathDataV x -> x
    _ -> error "getPathData: expected [PathData a] but got something else"

-- | Apply a function to each point in a path
mapPathData :: (Autofloat a) => (Pt2 a -> Pt2 a) -> PathData a -> PathData a
mapPathData f pd = map (mapPath f) pd

mapPath :: (Autofloat a) => (Pt2 a -> Pt2 a) -> Path' a -> Path' a
mapPath f (Closed es) = Closed $ map (mapElem f) es
mapPath f (Open es)   = Open $ map (mapElem f) es

mapElem :: (Autofloat a) => (Pt2 a -> Pt2 a) -> Elem a -> Elem a
mapElem f (Pt p) = Pt $ f p
mapElem f (CubicBez (p0, p1, p2)) = CubicBez (f p0, f p1, f p2)
mapElem f (CubicBezJoin (p0, p1)) = CubicBezJoin (f p0, f p1)
mapElem f (QuadBez (p0, p1)) = QuadBez (f p0, f p1)
mapElem f (QuadBezJoin p) = QuadBezJoin $ f p

-- | Add an offset to each point in a path
translatePath :: (Autofloat a) => Pt2 a -> PathData a -> PathData a
translatePath offset pd = mapPathData (+: offset) pd

-- | Figure out whether to translate pathManual or pathData
movePath :: (Autofloat a) => (a, a) -> Shape a -> Shape a
movePath offset shape =
      let pathManual = getPath shape
          pathData   = getPathData shape in
      case (pathManual, pathData) of
      (_:_, []) -> let res = (map (+: offset) pathManual) in
                   set shape "path" (PtListV res)
      ([], _:_) -> let res = translatePath offset pathData in
                   set shape "pathData" (PathDataV res)
      (_:_, _:_) -> error "shape can't have both path manually set and path data"
      ([], []) -> error "shape has no path or data"

-- | Transform utils
getTransform :: (Autofloat a) => Shape a -> HMatrix a
getTransform shape = case shape .: "transform" of
    HMatrixV x -> x
    _ -> error "getTransform: expected HMatrix but got something else"

getTransformation :: (Autofloat a) => Shape a -> HMatrix a
getTransformation shape = case shape .: "transformation" of
    HMatrixV x -> x
    _ -> error "getTransformation: expected HMatrix but got something else"

-- | ternary op for set (TODO: maybe later)
-- https://wiki.haskell.org/Ternary_operator

-- | HACK: returns true of a property of a shape is not supposed to be
-- | changed by the optimizer
isPending :: ShapeTypeStr -> PropID -> Bool
isPending typ propId = propId `elem` pendingProperties typ

-- | HACK: returns all "pending" properties that are undeterminded until
-- | rendered by the frontend
pendingProperties :: ShapeTypeStr -> [PropID]
pendingProperties "Text" = ["w", "h"]
pendingProperties "TextTransform" = ["w", "h"]
pendingProperties "ImageTransform" = ["initWidth", "initHeight"]
pendingProperties _ = []

-- | Given 'ValueType' and 'ShapeTypeStr', return all props of that ValueType
propertiesOf :: ValueType -> ShapeTypeStr -> [PropID]
propertiesOf propType shapeType =
    M.keys $ M.filter (\(t, _) -> t == propType) $ snd $ findDef shapeType

-- | Given 'ValueType' and 'ShapeTypeStr', return all props NOT of that ValueType
propertiesNotOf :: ValueType -> ShapeTypeStr -> [PropID]
propertiesNotOf propType shapeType =
    M.keys $ M.filter (\(t, _) -> t /= propType) $ snd $ findDef shapeType

-- filterProperties :: (Autofloat a) => ((ValueType, SampledValue a) -> Bool) -> [PropID]
-- filterProperties filterF =

-- | Map over all properties of a shape
-- TODO: withKey?
mapProperties :: (Autofloat a) => (Value a -> Value a) -> Shape a -> Shape a
mapProperties f (t, propDict) = (t, M.map f propDict)

-- | fold over all properties of a shape
-- TODO: withKey?
foldlProperties :: (Autofloat a) => (b -> Value a -> b) -> b -> Shape a -> b
foldlProperties f accum (_, propDict)  =  M.foldl' f accum propDict

foldlPropertyDefs :: (Autofloat a) => (b -> (ValueType, SampledValue a) -> b) -> b -> ShapeDef a -> b
foldlPropertyDefs f accum (_, propDict) =  M.foldl' f accum propDict

foldlPropertyMappings :: (Autofloat a) =>
    (b -> PropID -> (ValueType, SampledValue a) -> b) -> b -> ShapeDef a -> b
foldlPropertyMappings f accum (_, propDict) =  M.foldlWithKey f accum propDict

--------------------------------------------------------------------------------
-- Error Msgs

noShapeError functionName shapeType =
    error (functionName ++ ": Shape \"" ++ shapeType ++ "\" does not exist")
noPropError functionName prop shapeType =
    error (functionName ++ ": Property \"" ++ prop ++
        "\" does not exist in shape \"" ++ shapeType ++ "\"")


--------------------------------------------------------------------------------
-- Color definition
-- Adopted from gloss: https://github.com/benl23x5/gloss/blob/c63daedfe3b60085f8a9e810e1389cbc29110eea/gloss-rendering/Graphics/Gloss/Internals/Data/Color.hs

data Color
    -- | Holds the color components. All components lie in the range [0..1.
    = RGBA  !Float !Float !Float !Float
    deriving (Show, Eq, Generic)
instance ToJSON   Color
instance FromJSON Color

-- | Make a custom color. All components are clamped to the range  [0..1].
makeColor :: Float        -- ^ Red component.
          -> Float        -- ^ Green component.
          -> Float        -- ^ Blue component.
          -> Float        -- ^ Alpha component.
          -> Color
makeColor r g b a
        = clampColor
        $ RGBA r g b a
{-# INLINE makeColor #-}

-- | Take the RGBA components of a color.
rgbaOfColor :: Color -> (Float, Float, Float, Float)
rgbaOfColor (RGBA r g b a)      = (r, g, b, a)
{-# INLINE rgbaOfColor #-}

-- | Clamp components of a raw color into the required range.
clampColor :: Color -> Color
clampColor cc
   = let  (r, g, b, a)    = rgbaOfColor cc
     in   RGBA (min 1 r) (min 1 g) (min 1 b) (min 1 a)

black, white :: Color
black = makeColor 0.0 0.0 0.0 1.0
white = makeColor 1.0 1.0 1.0 1.0

makeColor' :: (Autofloat a) => a -> a -> a -> a -> Color
makeColor' r g b a = makeColor (r2f r) (r2f g) (r2f b) (r2f a)

--------------------------------------------------------------------------------
-- Approximating a Bezier curve via polygon or bbox

-- | Polygonize path by extruding the polyline to account for thickness and adding any arrowheads, if present.
-- For each point, find the corresponding inner and outer points on the extruded line
-- by taking the tangent and then taking the inner and outer normal
-- Note: not good for steep angles!
-- Then make the polygon by joining the points in order, accounting for arrowheads

-- TODO: (performance optimization) remove the ++, the accesses to last of list, and the reverses

polygonizePathPolygon :: Autofloat a => Int -> a -> Bool -> Bool -> PathData a -> [Pt2 a]
polygonizePathPolygon n thickness leftArr rightArr p = 
    let polyline = removeClosePoints $ polygonizePath n p
        tangents = map calcTangent $ zip polyline (tail polyline)
        tangentsWithLast = tangents ++ [last tangents] -- use the same tangent for the last point
        -- outer and inner are two "copies" of the polyline, offset in the +/- normal directions
        (outer, inner) = unzip $ map (extrude thickness) $ zip polyline tangentsWithLast
        snipSize = 4 * thickness 
       -- snipSize is the amount to remove from the polygonized curve to attach the arrows at either end

        (ltArrow, outer', inner') = 
            if not leftArr then ([], outer, inner)
            else let ltCenter = head polyline
                     ltAngle = atan2' $ head tangents
                     ltTransform = translationM ltCenter # rotationM ltAngle
                     ltArrow = transformSimplePoly ltTransform $ ltArrowheadPoly thickness
                     (outerFirst, innerFirst) = (head outer, head inner)
                     outer' = dropWhile (\x -> mag (x -: outerFirst) < snipSize) outer
                     inner' = dropWhile (\x -> mag (x -: innerFirst) < snipSize) inner
            in (ltArrow, outer', inner')

        (rtArrow, outer'', inner'') = 
            if not rightArr then ([], outer', inner') 
            else let rtCenter = last polyline
                     rtAngle = atan2' $ last tangents
                     rtTransform = translationM rtCenter # rotationM rtAngle
                     rtArrow = transformSimplePoly rtTransform $ rtArrowheadPoly thickness
                     (outerLast, innerLast) = (last outer, last inner)
                     outer'' = reverse $ dropWhile (\x -> mag (x -: outerLast) < snipSize) $ reverse outer'
                     inner'' = reverse $ dropWhile (\x -> mag (x -: innerLast) < snipSize) $ reverse inner'
            in (rtArrow, outer'', inner'')

        polygonWithArrows = outer'' ++ rtArrow ++ reverse inner'' ++ ltArrow

    in polygonWithArrows

    where atan2' (x, y) = atan2 y x

          removeClosePoints :: (Autofloat a) => [Pt2 a] -> [Pt2 a]
          -- Heuristic: if any two points are too close together, remove the first one
          -- Not super principled but just to make sure that the tangents don't NaN if two points are too close
          removeClosePoints ps = let ptsWithNext = zip ps (tail ps) in
                                 (map fst $ filter (\(p1, p2) -> mag (p2 -: p1) >= epsp) ptsWithNext) ++ [last ps]
                                 where epsp = 10 ** (-8)

          calcTangent :: (Autofloat a) => (Pt2 a, Pt2 a) -> Pt2 a
          calcTangent (start, end) = normalize' $ end -: start

          extrude :: (Autofloat a) => a -> (Pt2 a, Pt2 a) -> (Pt2 a, Pt2 a)
          extrude thickness (base, tangent) = 
                  let normal = thickness / 2.0 *: rot90 tangent in
                  (base +: normal, base -: normal)

-- | Polygonize path as polyline
polygonizePath :: Autofloat a => Int -> PathData a -> [Pt2 a]
polygonizePath n p = case polygonize n p of
                     [] -> error "empty curve: did you set the pathdata or path?"
                     x:xs -> x -- What are the other parts of this list?

polygonize :: Autofloat a => Int -> PathData a -> [[Pt2 a]]
polygonize maxIter = map go
    where
        go (Closed path) = error "TODO"
        go (Open path) = concatMap (polyCubicBez 0 maxIter) $ expandCurves path

type CubicBezCoeffs a = (Pt2 a, Pt2 a, Pt2 a, Pt2 a)

expandCurves :: Autofloat a => [Elem a] -> [CubicBezCoeffs a]
expandCurves elems = zipWith attach elems $ tail elems
    where
        attach (Pt a) (CubicBez (b, c, d)) = (a, b, c, d)
        attach (CubicBez (_, _, a)) (CubicBez (b, c, d)) = (a, b, c, d)

-- | implements http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.86.162&rep=rep1&type=pdf
polyCubicBez :: Autofloat a => Int -> Int -> CubicBezCoeffs a -> [Pt2 a]
polyCubicBez count maxCount curve@(a, b, c, d) =
    if (tr "count" count) >= maxCount then [a, b, c, d] else
        concatMapTuple (polyCubicBez (count + 1) maxCount) $ divideCubicBezier curve
    where concatMapTuple f (a1, a2) = f a1 ++ f a2

isFlat :: Autofloat a => CubicBezCoeffs a -> Bool
isFlat (a, b, c, d) = True

divideCubicBezier :: Autofloat a => CubicBezCoeffs a -> (CubicBezCoeffs a, CubicBezCoeffs a)
divideCubicBezier bezier@(a, _, _, d) = (left, right) where
    left = (a, ab, abbc, abbcbccd)
    right = (abbcbccd, bccd, cd, d)
    (ab, _bc, cd, abbc, bccd, abbcbccd) = splitCubicBezier bezier

--                     BC
--         B X----------X---------X C
--    ^     /      ___/   \___     \     ^
--   u \   /   __X------X------X_   \   / v
--      \ /___/ ABBC       BCCD  \___\ /
--    AB X/                          \X CD
--      /                              \
--     /                                \
--    /                                  \
-- A X                                    X D
splitCubicBezier :: Autofloat a => CubicBezCoeffs a -> (Pt2 a, Pt2 a, Pt2 a, Pt2 a, Pt2 a, Pt2 a)
splitCubicBezier (a, b, c, d) = (ab, bc, cd, abbc, bccd, abbcbccd)
    where
        ab = a `midpoint` b
        bc = b `midpoint` c
        cd = c `midpoint` d
        abbc = ab `midpoint` bc
        bccd = bc `midpoint` cd
        abbcbccd = abbc `midpoint` bccd

bezierBbox :: (Autofloat a) => Shape a -> ((a, a), (a, a)) -- poly Point type?
bezierBbox cb = let path = getPath cb
                    (xs, ys) = (map fst path, map snd path)
                    lower_left = (minimum xs, minimum ys)
                    top_right = (maximum xs, maximum ys) in
                (lower_left, top_right)

--------------------------------------------------------------------------------
-- DEBUG: main function to test out the module
--
-- main :: IO ()
-- main = do
--     let c = exampleCirc
--     print $ c .: "r"
--     let c' = set c "r" (FloatV 20)
--     print c'
--     let c'' = reset c "r"
--     print c''
--     print $ propertiesOf FloatT "Circle" shapeDefs