-- | Contains all the types and functions for composing
-- and rendering 2D graphics.
module Helm.Graphics2D
  (
    -- * Types
    Collage(..)
  , Form(..)
  , FormStyle(..)
  , FillStyle(..)
  , LineCap(..)
  , LineJoin(..)
  , LineStyle(..)
  , Path(..)
  , Shape(..)
  , ShapeStyle(..)
  , Transform(..)
  , Text(..)
  -- * Collages
  , collage
  , clip
  , center
  , toForm
  -- * Styles & Forms
  , defaultLine
  , solid
  , dashed
  , dotted
  , filled
  , textured
  , gradient
  , outlined
  , traced
  , image
  , fittedImage
  , croppedImage
  , blank
  , alpha
  , text
  -- * Grouping
  , group
  , groupTransform
  -- * Transforming
  , rotate
  , scale
  , move
  -- * Paths
  , path
  -- * Shapes
  , polygon
  , rect
  , square
  , oval
  , circle
  , ngon
  ) where

import Linear.V2 (V2(V2))

import Helm.Asset (Image)
import Helm.Color (Color, rgb, Gradient)
import Helm.Graphics2D.Text (Text(..))
import Helm.Graphics2D.Transform (Transform(..), identity)

-- | Represents a collection of forms, which in turn are rendereable
-- shapes and lines. In Helm, the collage is the main structure
-- representing 2D graphics and is passed directly to the engine
-- to be rendered by your view function. It's best to think of a collage
-- as a fancy version of a game screen, with the difference being that the
-- collage itself knows nothing about the window state. It only knows
-- what will be rendered to the screen (which in this case, is a series of forms)
-- and the order in which they will be rendered.
data Collage e = Collage
  { collageDims :: Maybe (V2 Double)    -- ^ The optional dimensions of the collage. It will be clipped to these dims.
  , collageForms :: [Form e]            -- ^ The collection of forms under the collage.
  , collageCenter :: Maybe (V2 Double)  -- ^ The optional center of the collage.
  }

-- | Create a collage from a list of forms.
-- By default, the collage will not be clipped
-- and will not be centered. The origin point of the contained
-- forms will be the top-left of the collage (which in the case of rendering
-- a collage to the screen, is coincidently the top-left of the game window).
-- See 'center' and 'clip'.
collage :: [Form e] -> Collage e
collage forms = Collage
  { collageDims = Nothing
  , collageForms = forms
  , collageCenter = Nothing
  }

-- | Center a collage around a fixed point. This is useful to implement
-- 2D game cameras - usually, you have the center of the screen
-- at the position of the game camera (which in a 2D platformer,
-- is usually your game character). Note that this will center
-- the forms themselves, i.e. their original point will change from being
-- the top left of the collage to
center ::
     V2 Double  -- ^ The position to center the collage at.
  -> Collage e  -- ^ The source collage.
  -> Collage e  -- ^ The centered collage.
center pos col = col { collageCenter = Just pos }

-- | Clip a collage by provided dimensions. Note that by default,
-- a collage will not be clipped and anything beyond the window dimensions
-- will still technically be rendered (although obviously it will not appear
-- on the game screen). By composing a collage with this function,
-- when the collage is rendered its contents will be clipped by these dimensions.
-- Not only will this generally speed up performance, it can be used for certain
-- cases where you don't want the forms in the collage to spill over
-- to other collages near it (but that's a very rare use-case).
--
-- Something to note, that this is absolutely not an ensurance that your 2D graphics
-- will be rendered quickly if you're doing a lot of graphics work. The clip merely
-- prevents things being drawn outside the dimensions, which in most cases will
-- indeed speed up the performance, but it is down to the engine implementation for how much
-- this actually helps.
--
-- In that sense, it's up to the library user to make sure they're not rendering huge amounts
-- of forms that aren't even in the screen's bounds.
clip ::
     V2 Double  -- ^ The dimensions to clip the collage with.
  -> Collage e  -- ^ The source collage.
  -> Collage e  -- ^ The clipped collage.
clip dims col = col { collageDims = Just dims }

-- | Create a form from a collage. This might seem a little strange (as
-- a collage is generally what you provide to the engine to render the 2D graphics)
-- but by allowing this functionality, you can compose collages from other collages.
toForm :: Collage e -> Form e
toForm = defaultForm . CollageForm

-- | Represents the styles of forms available. The form style holds data specific
-- to a variation of form, and the 'Form' is instead a general version of this
-- with positioning information, rotation, scale, etc.
data FormStyle e
  = PathForm LineStyle Path                           -- ^ A form composed of a path
  | ShapeForm (ShapeStyle e) Shape                    -- ^ A form composed of a shape.
  | TextForm Text                                     -- ^ A form composed of a piece of text, including string and style info.
  | ImageForm (Image e) (V2 Double) (V2 Double) Bool  -- ^ A form composed of an image
  | GroupForm Transform [Form e]                      -- ^ A form composed of a group of forms, with a transformation.
  | CollageForm (Collage e)                           -- ^ A form composed of a collage (which in turn is a collection of forms).

-- | Represents something that can be rendered to the screen (
-- contained under a collage). There are many different types of forms, which can be composed
-- below but are generally represented by the 'FormStyle' type.
--
-- A form might be an image, or a rectangle, or a circle, or even a collection
-- of forms (which in turn can be those same things).
data Form e = Form
  { formTheta :: Double       -- ^ The rotation of the form (in radians).
  , formScale :: Double       -- ^ The scale factor of the form.
  , formPos :: V2 Double      -- ^ The position of the form. This will be rendered relative to the collage origin.
  , formAlpha :: Double       -- ^ The alpha channel of the form.
  , formStyle :: FormStyle e  -- ^ The style of form.
  }

-- | Represents the style of shape filling available.
data FillStyle e
  = Solid Color        -- ^ The shape will be filled with a solid color.
  | Texture (Image e)  -- ^ The shape will be filled with a texture (a.k.a. image).
  | Gradient Gradient  -- ^ The shape will be filled with a gradient (which can be linear or radial).

-- | Represents the shape of the ends of a line.
data LineCap
  = FlatCap
  | RoundCap
  | PaddedCap
    deriving (Show, Eq, Ord, Read)

-- | Represents the shape of the joints between line segments.
data LineJoin
  = SmoothJoin
  | SharpJoin Double
  | ClippedJoin
    deriving (Show, Eq, Ord, Read)

-- | Represents the style used for drawing lines. It's best
-- to use 'defaultLine' and then only change the fields
-- you need to.
data LineStyle = LineStyle
  { lineColor :: Color
  , lineWidth :: Double
  , lineCap :: LineCap
  , lineJoin :: LineJoin
  , lineDashing :: [Double]
  , lineDashOffset :: Double
  } deriving (Show, Eq)

-- | Represents a series of 2D points which will be drawn in sequence.
-- Like a 'Shape', a path on its own holds no styling information.
data Path = Path [V2 Double] deriving (Show, Eq, Ord, Read)

-- | Create a path from a sequence of points (represented as 2D vectors).
path :: [V2 Double] -> Path
path = Path

-- | Represents a collection of points that when drawn in order
-- will result in a closed polygon. They have no style information -
-- rather, you compose shapes into a form with fill or line style
-- and that affects their appearance.
--
-- Note that realistically, a shape could be represented as
-- a path only. However, we add extra variants here as drawing backends
-- usually provide optimized forms of drawing circles (and perhaps rectangles,
-- although that is less likely) hence it's better to fall to those
-- if our shape is circular.
data Shape
  = PolygonShape Path
  | RectangleShape (V2 Double)
  | ArcShape (V2 Double) Double Double Double (V2 Double)
    deriving (Show, Eq, Ord, Read)

-- | Create an arbitary-sided polygon from a path.
-- The points provided should refer to each corner of the -gon,
-- however the points do not need to loop around (i.e. the final point
-- will automatically connect to the first point).
polygon :: Path -> Shape
polygon = PolygonShape

-- | Create a rectangular shape from a 2D vector, with
-- x and y representing width and height, respectively.
rect :: V2 Double -> Shape
rect = RectangleShape

-- | Create a square shape with a side length.
square :: Double -> Shape
square n = rect (V2 n n)

-- | Create an oval shape with a width and height.
oval :: Double -> Double -> Shape
oval w h = ArcShape (V2 0 0) 0 (2 * pi) 1 (V2 (w / 2) (h / 2))

-- | Create a circle shape with a radius.
circle :: Double -> Shape
circle r = ArcShape (V2 0 0) 0 (2 * pi) r (V2 1 1)

-- | Create a generic n-sided polygon (e.g. octagon, pentagon, etc) with
-- a side count and radius.
ngon :: Int -> Double -> Shape
ngon n r = polygon $ path $ map point series
  where
    point i = V2 (r * cos (t * i)) (r * sin (t * i))
    series = [0 .. fromIntegral (n - 1)]
    m = fromIntegral n
    t = 2 * pi / m

-- | Create the default line style. By default, the line is black with a width of 1,
-- flat caps and regular sharp joints.
defaultLine :: LineStyle
defaultLine = LineStyle
  { lineColor = rgb 0 0 0
  , lineWidth = 1
  , lineCap = FlatCap
  , lineJoin = SharpJoin 10
  , lineDashing = []
  , lineDashOffset = 0
  }

-- | Create a initial form from a specific form style.
-- The form will be at the origin point (0, 0).
defaultForm :: FormStyle e -> Form e
defaultForm style = Form
  { formTheta = 0
  , formScale = 1
  , formPos = V2 0 0
  , formAlpha = 1
  , formStyle = style
  }

-- | Represents the style used for drawing a shape.
data ShapeStyle e
  = OutlinedShape LineStyle    -- ^ Stroke/outline the shape, with a specific line style.
  | FilledShape (FillStyle e)  -- ^ Fill the shape, with a specific fill style.

-- | Create a solid line style with a color.
solid :: Color -> LineStyle
solid color = defaultLine { lineColor = color }

-- | Create a dashed line style with a color.
dashed :: Color -> LineStyle
dashed color = defaultLine { lineColor = color, lineDashing = [8, 4] }

-- | Create a dotted line style with a color.
dotted :: Color -> LineStyle
dotted color = defaultLine { lineColor = color, lineDashing = [3, 3] }

-- | Fill a shape with a specific fill style.
fill :: FillStyle e -> Shape -> Form e
fill style shape = defaultForm (ShapeForm (FilledShape style) shape)

-- | Fill a shape with a color.
filled :: Color -> Shape -> Form e
filled color = fill (Solid color)

-- | Fill a shape with a texture. The texture should
-- be an image loaded by the engine.
textured :: Image e -> Shape -> Form e
textured img = fill (Texture img)

-- | Fill a shape with a gradient (either 'linear' or 'radial').
gradient :: Gradient -> Shape -> Form e
gradient grad = fill (Gradient grad)

-- | Create a form from a shape by outlining it with a specific line style.
outlined :: LineStyle -> Shape -> Form e
outlined style shape = defaultForm (ShapeForm (OutlinedShape style) shape)

-- | Create a form from a path by tracing it with a specific line style.
traced :: LineStyle -> Path -> Form e
traced style p = defaultForm (PathForm style p)

-- | Create an empty form, useful for having forms rendered only at some state.
blank :: Form e
blank = group []

-- | Create a form from an image. If the image dimensions are not the
-- same as provided, then it will stretch/shrink to fit.
image :: V2 Double -> Image e -> Form e
image dims img = defaultForm $ ImageForm img (V2 0 0) dims True

-- | Create a form from an image with a 2D vector describing its dimensions.
-- If the image dimensions are not the same as given, then it will only use the relevant pixels
-- (i.e. cut out the given dimensions instead of scaling). If the given dimensions are bigger than
-- the actual image, than irrelevant pixels are ignored.
fittedImage :: V2 Double -> Image e -> Form e
fittedImage dims img = defaultForm $ ImageForm img (V2 0 0) dims False

-- | Create a form from an image by cropping it with a certain position, width, height
-- and image file path. This can be used to divide a single image up into smaller ones (
-- for example, drawing a single sprite from a sprite sheet).
croppedImage :: V2 Double -> V2 Double -> Image e -> Form e
croppedImage pos dims img = defaultForm $ ImageForm img pos dims False

-- | Group a list of forms into one. They will be drawn in their
-- sequential order within the list.
group :: [Form e] -> Form e
group forms = defaultForm (GroupForm identity forms)

-- | Group a list of forms into one, while also applying a matrix
-- transformation.
groupTransform :: Transform -> [Form e] -> Form e
groupTransform matrix forms = defaultForm (GroupForm matrix forms)

-- | Move a form by a given 2D vector. The movement is relative,
-- i.e. the translation vector provided will be added to the form's
-- current position.
move :: V2 Double -> Form e -> Form e
move trans form = form { formPos = formPos form + trans }

-- | Scale a form by a scalar factor. Scaling by 2 will double the size
-- of the form, and scaling by 0.5 will half the size. Note that like
-- 'move', the scale function is relative - i.e. if you scaled by 0.5
-- and then scaled by 0.5 a gain, the final scale would be 0.25 or
-- a quarter of the form's initial scale.
scale :: Double -> Form e -> Form e
scale factor form = form { formScale = factor * formScale form }

-- | Rotate a form by a given angle (in radians).
-- Like 'move' and 'scale', the rotation is relative.
rotate :: Double -> Form e -> Form e
rotate theta form = form { formTheta = formTheta form + theta }

-- | Change the alpha value of a form (i.e. its transparency).
-- By default, forms will have an alpha value of 1, in other words,
-- they are fully opaque. Alternatively, a value of 0 will mean the
-- form is completely hidden.
alpha :: Double -> Form e -> Form e
alpha x form = form { formAlpha = x }

-- | Create a form from a `Text` structure, which in turn
-- contains all of the text values and styling. This allows
-- you to render a the text graphically (and in turn it's a regular old
-- form, so it can be translated, rotated, etc.).
text :: Text -> Form e
text = defaultForm . TextForm