/*
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include "click.h"
#include "content_indicator.h"
#include "layer.h"
#include "property_animation.h"

//! @file scroll_layer.h
//! @addtogroup UI
//! @{
//!   @addtogroup Layer Layers
//!   @{
//!     @addtogroup ScrollLayer
//! \brief Layer that scrolls its contents, animated.
//!
//! ![](scroll_layer.png)
//! <h3>Key Points</h3>
//! * Facilitates vertical scrolling of a layer sub-hierarchy zero or more
//! arbitrary layers. The example image shows a scroll layer containing one
//! large TextLayer.
//! * Shadows to indicate that there is more content are automatically drawn
//! on top of the content. When the end of the scroll layer is reached, the
//! shadow will automatically be retracted.
//! * Scrolling from one offset to another is animated implicitly by default.
//! * The scroll layer contains a "content" sub-layer, which is the layer that
//! is actually moved up an down. Any layer that is a child of this "content"
//! sub-layer, will be moved as well. Effectively, an entire layout of layers
//! can be scrolled this way. Use the convenience function
//! \ref scroll_layer_add_child() to add child layers to the "content" sub-layer.
//! * The scroll layer needs to be informed of the total size of the contents,
//! in order to calculate from and to what point it should be able to scroll.
//! Use \ref scroll_layer_set_content_size() to set the size of the contents.
//! * The button behavior is set up, using the convenience function
//! \ref scroll_layer_set_click_config_onto_window(). This will associate the
//! UP and DOWN buttons with scrolling up and down.
//! * The SELECT button can be configured by installing a click configuration
//! provider using \ref scroll_layer_set_callbacks().
//! * To scroll programmatically to a certain offset, use
//! \ref scroll_layer_set_content_offset().
//! * It is possible to get called back for each scrolling increment, by
//! installing the `.content_offset_changed_handler` callback using
//! \ref scroll_layer_set_callbacks().
//! * Only vertical scrolling is supported at the moment.
//!     @{

struct Window;
struct ScrollLayer;

//! Function signature for the `.content_offset_changed_handler` callback.
typedef void (*ScrollLayerCallback)(struct ScrollLayer *scroll_layer, void *context);

//! All the callbacks that the ScrollLayer exposes for use by applications.
//! @note The context parameter can be set using scroll_layer_set_context() and
//! gets passed in as context with all of these callbacks.
typedef struct ScrollLayerCallbacks {

  //! Provider function to set up the SELECT button handlers. This will be
  //! called after the scroll layer has configured the click configurations for
  //! the up/down buttons, so it can also be used to modify the default up/down
  //! scrolling behavior.
  ClickConfigProvider click_config_provider;

  //! Called every time the the content offset changes. During a scrolling
  //! animation, it will be called for each intermediary offset as well
  ScrollLayerCallback content_offset_changed_handler;

} ScrollLayerCallbacks;

//! Data structure of a scroll layer.
//! @note a `ScrollLayer *` can safely be casted to a `Layer *` and can thus be
//! used with all other functions that take a `Layer *` as an argument.
//! <br/>For example, the following is legal:
//! \code{.c}
//! ScrollLayer scroll_layer;
//! ...
//! layer_set_hidden((Layer *)&scroll_layer, true);
//! \endcode
//! @note However, there are a few caveats:
//! * To add content layers, you must use \ref scroll_layer_add_child().
//! * To change the frame of a scroll layer, use \ref scroll_layer_set_frame().
//! * Do not try to change to bounds of a scroll layer.
typedef struct ScrollLayer {
  //! @internal
  //! Empty container layer that is parent to the content & shadow sublayers.
  Layer layer;

  //! @internal
  //! The layer that is actually scrolled. Use scroll_layer_add_child() to add
  //! your content.
  Layer content_sublayer;

  union {
    //! @internal
    //! Layer that draws "more content" shadows when appropriate.
    Layer shadow_sublayer;  // deprecated
    //! @internal
    //! Paging data aligned with shadow_sublayer struct to save bytes
    //! Do not change the position or size of these items
    struct ScrollPaging {
      uint8_t padding[16];  // padding to align paging data with shadow_sublayer
      union {
        uint8_t flags;
        struct {
          // Do not move these bit-fields!
          bool paging_disabled:1;  // mutually exclusive of shadow clips
          bool shadow_hidden:1;  // shadow_hidden in same position as layer hidden
        };
      };
    } paging;
  };

  //! @internal
  //! Scrolling animation state. Configured automatically.
  PropertyAnimation *animation;

  //! @internal
  //! Application supplied callbacks.
  //! Use \ref scroll_layer_set_callbacks() to assign the callbacks.
  ScrollLayerCallbacks callbacks;

  //! @internal
  //! Application supplied callback context.
  //! Use \ref scroll_layer_set_context() to assign the callback context.
  void *context;
} ScrollLayer;

#ifndef __clang__
_Static_assert(offsetof(struct ScrollPaging, flags) == offsetof(Layer, flags),
               "ScrollPaging struct alignment with shadow_sublayer broken");
#endif

//! @internal
//! Initializes the given scroller, sets its frame and bounds and resets it
//! to the defaults:
//! * Clips: `true`
//! * Hidden: `false`
//! * Content size: `frame.size`
//! * Content offset: \ref GPointZero
//! * Callbacks: None (`NULL` for each one)
//! * Callback context: `NULL`
//! The layer is marked dirty automatically.
//! @param scroll_layer The ScrollLayer to initialize
//! @param frame The frame with which to initialize the ScrollLayer
void scroll_layer_init(ScrollLayer *scroll_layer, const GRect *frame);

//! Creates a new ScrollLayer on the heap and initializes it with the default values:
//! * Clips: `true`
//! * Hidden: `false`
//! * Content size: `frame.size`
//! * Content offset: \ref GPointZero
//! * Callbacks: None (`NULL` for each one)
//! * Callback context: `NULL`
//! @return A pointer to the ScrollLayer. `NULL` if the ScrollLayer could not
//! be created
ScrollLayer* scroll_layer_create(GRect frame);

void scroll_layer_deinit(ScrollLayer *scroll_layer);

//! Destroys a ScrollLayer previously created by scroll_layer_create.
void scroll_layer_destroy(ScrollLayer *scroll_layer);

//! Gets the "root" Layer of the scroll layer, which is the parent for the sub-
//! layers used for its implementation.
//! @param scroll_layer Pointer to the ScrollLayer for which to get the "root" Layer
//! @return The "root" Layer of the scroll layer.
//! @internal
//! @note The result is always equal to `(Layer *) scroll_layer`.
Layer* scroll_layer_get_layer(const ScrollLayer *scroll_layer);

//! Adds the child layer to the content sub-layer of the ScrollLayer.
//! This will make the child layer part of the scrollable contents.
//! The content sub-layer of the ScrollLayer will become the parent of the
//! child layer.
//! @param scroll_layer The ScrollLayer to which to add the child layer.
//! @param child The Layer to add to the content sub-layer of the ScrollLayer.
//! @note You may need to update the size of the scrollable contents using
//! \ref scroll_layer_set_content_size().
void scroll_layer_add_child(ScrollLayer *scroll_layer, Layer *child);

//! Convenience function to set the \ref ClickConfigProvider callback on the
//! given window to scroll layer's internal click config provider. This internal
//! click configuration provider, will set up the default UP & DOWN
//! scrolling behavior.
//! This function calls \ref window_set_click_config_provider_with_context to
//! accomplish this.
//!
//! If you application has set a `.click_config_provider`
//! callback using \ref scroll_layer_set_callbacks(), this will be called
//! by the internal click config provider, after configuring the UP & DOWN
//! buttons. This allows your application to configure the SELECT button
//! behavior and optionally override the UP & DOWN
//! button behavior. The callback context for the SELECT click recognizer is
//! automatically set to the scroll layer's context (see
//! \ref scroll_layer_set_context() ). This context is passed into
//! \ref ClickHandler callbacks. For the UP and DOWN buttons, the scroll layer
//! itself is passed in by default as the callback context in order to deal with
//! those buttons presses to scroll up and down automatically.
//! @param scroll_layer The ScrollLayer that needs to receive click events.
//! @param window The window for which to set the click configuration.
//! @see \ref Clicks
//! @see window_set_click_config_provider_with_context
void scroll_layer_set_click_config_onto_window(ScrollLayer *scroll_layer, struct Window *window);

//! Sets the callbacks that the scroll layer exposes.
//! The `context` as set by \ref scroll_layer_set_context() is passed into each
//! of the callbacks. See \ref ScrollLayerCallbacks for the different callbacks.
//! @note If the `context` is NULL, a pointer to scroll_layer is used
//! as context parameter instead when calling callbacks.
//! @param scroll_layer The ScrollLayer for which to assign new callbacks.
//! @param callbacks The new callbacks.
void scroll_layer_set_callbacks(ScrollLayer *scroll_layer, ScrollLayerCallbacks callbacks);

//! Sets a new callback context. This context is passed into the scroll layer's
//! callbacks and also the \ref ClickHandler for the SELECT button.
//! If `NULL` or not set, the context defaults to a pointer to the ScrollLayer
//! itself.
//! @param scroll_layer The ScrollLayer for which to assign the new callback
//! context.
//! @param context The new callback context.
//! @see scroll_layer_set_click_config_onto_window
//! @see scroll_layer_set_callbacks
void scroll_layer_set_context(ScrollLayer *scroll_layer, void *context);

//! Scrolls to the given offset, optionally animated.
//! @note When scrolling down, the offset's `.y` decrements. When scrolling up,
//! the offset's `.y` increments. If scrolled completely to the top, the offset
//! is \ref GPointZero.
//! @note The `.x` field must be `0`. Horizontal scrolling is not supported.
//! @param scroll_layer The ScrollLayer for which to set the content offset
//! @param offset The final content offset
//! @param animated Pass in `true` to animate to the new content offset, or
//! `false` to set the new content offset without animating.
//! @see scroll_layer_get_content_offset
void scroll_layer_set_content_offset(ScrollLayer *scroll_layer, GPoint offset, bool animated);

//! Gets the point by which the contents are offset.
//! @param scroll_layer The ScrollLayer for which to get the content offset
//! @see scroll_layer_set_content_offset
GPoint scroll_layer_get_content_offset(ScrollLayer *scroll_layer);

//! Sets the size of the contents layer. This determines the area that is
//! scrollable. At the moment, this needs to be set "manually" and is not
//! derived from the geometry of the contents layers.
//! @param scroll_layer The ScrollLayer for which to set the content size.
//! @param size The new content size.
//! @see scroll_layer_get_content_size
void scroll_layer_set_content_size(ScrollLayer *scroll_layer, GSize size);

//! Gets the size of the contents layer.
//! @param scroll_layer The ScrollLayer for which to get the content size
//! @see scroll_layer_set_content_size
GSize scroll_layer_get_content_size(const ScrollLayer *scroll_layer);

//! Set the frame of the scroll layer and adjusts the internal layers' geometry
//! accordingly. The scroll layer is marked dirty automatically.
//! @param scroll_layer The ScrollLayer for which to set the frame
//! @param frame The new frame
void scroll_layer_set_frame(ScrollLayer *scroll_layer, GRect frame);

//! The click handlers for the UP button that the scroll layer will install as
//! part of \ref scroll_layer_set_click_config_onto_window().
//! @note This handler is exposed, in case one wants to implement an alternative
//! handler for the UP button, as a way to invoke the default behavior.
//! @param recognizer The click recognizer for which the handler is called
//! @param context A void pointer to the ScrollLayer that is the context of the click event
void scroll_layer_scroll_up_click_handler(ClickRecognizerRef recognizer, void *context);

//! The click handlers for the DOWN button that the scroll layer will install as
//! part of \ref scroll_layer_set_click_config_onto_window().
//! @note This handler is exposed, in case one wants to implement an alternative
//! handler for the DOWN button, as a way to invoke the default behavior.
//! @param recognizer The click recognizer for which the handler is called
//! @param context A void pointer to the ScrollLayer that is the context of the click event
void scroll_layer_scroll_down_click_handler(ClickRecognizerRef recognizer, void *context);

//! Sets the visibility of the scroll layer shadow.
//! If the visibility has changed, \ref layer_mark_dirty() will be called automatically
//! on the scroll layer.
//! @param scroll_layer The scroll layer for which to set the shadow visibility
//! @param hidden Supply `true` to make the shadow hidden, or `false` to make it
//! non-hidden.
void scroll_layer_set_shadow_hidden(ScrollLayer *scroll_layer, bool hidden);

//! Gets the visibility of the scroll layer shadow.
//! @param scroll_layer The scroll layer for which to get the visibility
//! @return True if the shadow is hidden, false if it is not hidden.
bool scroll_layer_get_shadow_hidden(const ScrollLayer *scroll_layer);

//! @internal
void scroll_layer_scroll(ScrollLayer *scroll_layer, ScrollDirection direction, bool animated);

//! Enables or disables paging of the ScrollLayer (default: disabled). When enabled, every button
//! press will change the scroll offset by the frame's height.
//! @param scroll_layer The scroll layer for which to enable or disable paging
//! @param paging_enabled True, if paging should be enabled. False to enable.
void scroll_layer_set_paging(ScrollLayer *scroll_layer, bool paging_enabled);

//! Check whether or not the ScrollLayer uses paging when pressing buttons.
//! @param scroll_layer The scroll layer for which to get the paging behavior.
//! @return True, if paging is enabled; false otherwise.
bool scroll_layer_get_paging(ScrollLayer *scroll_layer);

//! Gets the ContentIndicator for a ScrollLayer.
//! @param scroll_layer The ScrollLayer for which to get the ContentIndicator
//! @return A pointer to the ContentIndicator, or `NULL` upon failure.
ContentIndicator *scroll_layer_get_content_indicator(ScrollLayer *scroll_layer);

//! @internal
//! Updates the ContentIndicator for a ScrollLayer.
//! @param scroll_layer The ScrollLayer for which to update the ContentIndicator
void scroll_layer_update_content_indicator(ScrollLayer *scroll_layer);

//! @internal
//! Controls if the ScrollLayer respects its content_size when scrolling (default: true).
//! If set to false, any value for content_offset will be accepted.
//! @see \ref scroll_layer_get_clips_content_offset
//! @see \ref scroll_layer_set_content_offset
void scroll_layer_set_clips_content_offset(ScrollLayer *scroll_layer, bool clips);

//! @internal
//! True, if the ScrollLayer respects the content_size when scrolling; otherwise, false.
//! @see \ref scroll_layer_get_clips_content_offset
bool scroll_layer_get_clips_content_offset(ScrollLayer *scroll_layer);

//! @internal
//! True, if the passed layer is a scroll_layer; false otherwise.
bool scroll_layer_is_instance(const Layer *layer);

//!     @} // end addtogroup ScrollLayer
//!   @} // end addtogroup Layer
//! @} // end addtogroup UI
