<?php
/**
 * Base class for bridging VTCore HTML class and it's subclasses
 * into WordPress Shortcode API.
 *
 * Adding a new shortcode class process :
 *
 * 1. Add the new shortcode base into $shortcode either via the VTCore init shortcodes factory
 *    or do it via WordPress filter vtcore_register_shortcode.
 * 2. If the subclasses have different naming convention other than VTCore_Wordpress_Shortcodes_
 *    then it must register the overloader naming via $overloaderPrefix or via
 *    WordPress filter vtcore_register_shortcode_prefix or via VTCore init shortcode factory
 * 3. The shortcode subclass must handle its own autoloading mechanism if it is not
 *    using the VTCore autoloading class.
 * 4. Create the subclasses that defines the shortcode (look at the api folder for example)
 *
 * @author jason.xie@victheme.com
 */
abstract class VTCore_Wordpress_Models_Shortcodes {

  /**
   * All subclasses must store their object in this variable
   */
  protected $object;


  /**
   * Subclasses can define attributes with camel case that
   * should be preserved at all cost.
   *
   * The convertion will be invoked in the __call() method.
   */
  protected $camelcase = array();


  /**
   * Variable for storing shortcode attributes
   */
  protected $atts = array();


  /**
   * Subclass should store their default data here
   */
  protected $data = array();



  /**
   * Subclass can define settings arrays
   * this is usefull for jQuery plugin that has
   * camel case for the settings which cannot
   * use data- method
   */
  protected $settings = array();



  /**
   * Subclass can defined the attributes key
   * that must be converted to booleans by
   * supplying the key of the attributes array
   * in this array.
   */
  protected $booleans = array();



  /**
   * Marker for auto processing tags, set this to
   * false in the child element if it doesn't want
   * auto tag processing.
   */
  protected $processTag = true;



  /**
   * Marker for auto converting data into attributes
   * data with key prefix.
   */
  protected $convertDataToAttributes = true;


  /**
   * Marker for converting attributes from invalid
   * shortcode due to use of dotted notation into
   * a valid shortcode attributes array.
   *
   * Dotted notation support . and # as the array
   * breaker.
   */
  protected $processDottedNotation = false;

  /**
   * Constructor method, this is important because
   * we need to preprocess the atts to match HtmL object
   * and its subclasses context rules
   *
   * @param array $atts  raw attributes provided by WordPress Shortcode API
   * @param string $content  raw content provided by WordPress Shortcode API
   */
  public function __construct($atts = array(), $content = NULL) {
    $this->content = $content;
    $this->preprocessAtts($atts);
  }




  /**
   * Returning the markup for the stored object
   */
  public function getMarkup() {

    // @hook allow other class or plugin to do final alteration
    //       before the object is rendered.
    do_action('vtcore_wordpress_shortcode_object_alter', $this->object);

    return (is_object($this->object)) ?  $this->object->__toString() : $this->object;
  }



  /**
   * Some jQuery plugin is very picky about camelcase data
   * attributes. this function will preserve the original
   * camelcase taken from shortcode atts.
   *
   * Sub class must define the protected $camelcase variable
   * to let the base class undertand on what to preserve.
   *
   * @param array $atts
   * @return array
   */
  private function preserveCamelCase() {
    foreach ($this->camelcase as $case) {
      $name = strtolower($case);
      if (isset($this->atts[$name])) {
        $this->atts[$case] = $this->atts[$name];
        unset($this->atts[$name]);
      }
    }
  }





  /**
   * VTCore don't allow string as class
   * convert them into arrays.
   */
  private function processClass() {
    if (isset($this->atts['class'])) {
      $classes = explode(' ', $this->atts['class']);

      if (is_array($classes)) {
        foreach ($classes as $class) {
          $this->atts['attributes']['class']['custom-' . $class] = $class;

          if ($class == 'equalheightRow') {
            VTCore_Wordpress_Utility::loadAsset('jquery-equalheight');
          }
        }
      }

      unset($this->atts['class']);
    }
  }



  /**
   * Process attributes ID
   */
  private function processID() {
    if (isset($this->atts['id'])) {
      $this->atts['attributes']['id'] = str_replace(' ', '-', $this->atts['id']);
      unset($this->atts['id']);
    }
  }




  /**
   * Preprocess bootstrap grids
   */
  private function processGrid() {
    $gridKeys = array(
      'columns',
      'push',
      'pull',
      'offset',
      'hidden',
      'visible',
    );

    if (!empty($this->atts) && is_array($this->atts)) {

      foreach ($this->atts as $key => $value) {

        if (strpos($key, '_') === false) {
          continue;
        }

        list($type, $name) = explode('_', $key);
        if (in_array($type, $gridKeys)) {
          $this->atts['grids'][$type][$name] = $value;
        }
      }

    }
  }



  /**
   * Preprocess data attributes, this
   * method is useful for creating data-x attributes
   * as defined in the $data and $atts.
   */
  protected function processData() {

    foreach ($this->data as $key => $value) {
      if (isset($this->atts[$key])) {
        $this->atts['data'][$key] = $this->atts[$key];

        if ($this->convertDataToAttributes) {
          $this->atts['attributes']['data-' . $key] = $this->atts[$key];
        }

        unset($this->atts[$key]);
      }
    }
  }




  /**
   * Preprocess Settings
   * The markup will be data-settings and in json format
   */
  private function processSetting() {
    $settings = array();
    foreach ($this->settings as $key) {
      if (isset($this->atts[$key])) {
        $settings[$key] = $this->atts[$key];
      }
    }

    if (!empty($settings)) {
      $this->atts['data']['settings'] = json_encode($settings);
    }
  }



  /**
   * Preprocess shortcode tag attributes and assign
   * the value as the object tags.
   */
  private function processTag() {
    if (isset($this->atts['tag']) && !empty($this->atts['tag']) && $this->processTag == true) {
      $this->atts['type'] = $this->atts['tag'];
    }
  }

  /**
   * Preprocess inline styles
   * shortcode must utilize style params with normal inline
   * style in the format of style:value; to utilize this
   * methor properly.
   */
  private function processInlineStyle() {
    if (isset($this->atts['style']) && !empty($this->atts['style'])) {
      $styles = explode(';', $this->atts['style']);
      foreach ($styles as $style) {
        if (strpos($style, ':') !== false) {
          list($key, $value) = explode(':', $style);
          $this->atts['styles'][trim($key)] = trim($value);
        }
      }
    }
  }


  /**
   * Process dotted notation and explode them
   * into real arrays
   *
   * Wordpress by default will failed to parse
   * the attributes due to the character used
   * as dotted notation breaker (. or #) is treated
   * as invalid character.
   *
   * The original array will be a numeric keyed
   * array with the verbatim attributes string, example
   *
   * array(
   *  0 => 'somekey#somechildkey#somegranchildkey="somevalue"',
   * );
   *
   * Visual Composer as of 4.4.3 doesn't support . or # as
   * the breaker, use ___ instead (triple underscore).
   *
   * This method supports both . and # as the breaker.
   */
  private function processDottedNotation() {

    if ($this->processDottedNotation) {
      foreach ($this->atts as $key => $value) {

        // Wordpress convert dotted to numerical array, convert
        // it to dotted key and value
        if (is_numeric($key)) {
          list($dottedKey, $dottedValue) = explode('=', $value);

          // Visual composer can't handle anything except underscore.
          if (strpos($dottedKey, '___') !== false) {
            $dottedKey = str_replace('___', '.', $dottedKey);
          }

          $this->remove($key);
          $this->add($dottedKey, substr($dottedValue, 1, -1));
        }

        // Normal dotted, direct injecting
        elseif (strpos($key, '#') !== false || strpos($key, '.') !== false) {
          $this->remove($key);
          $this->add($key, $value);
        }

        // Handling VisualComposer, only underscore allowed
        elseif (strpos($key, '___') !== false) {
          $dottedKey = str_replace('___', '.', $key);
          $this->remove($key);
          $this->add($dottedKey, $value);
        }

      }
    }
  }


  /**
   * Preprocess booleans
   * This method will search for attributes as specified
   * in the $booleans class variables and attempt to
   * convert value such as "true", "false", 0, 1 into
   * the corresponding booleans
   */
  private function processBooleans() {
    foreach ($this->booleans as $key) {
      if ($this->get($key)) {
        $this->add($key, filter_var($this->get($key), FILTER_VALIDATE_BOOLEAN));
      }
    }
  }


 /**
  * Custom preprocess function, this is meant to be
  * extended by subclass if needed.
  */
  protected function processCustomRules() {}



  /**
   * Preprocess shortcode attributes first
   * as many jQuery plugin is very picky about the data structure.
   */
  private function preprocessAtts($atts) {

    $this->atts = $atts;

    $this->processDottedNotation();
    $this->preserveCamelCase();
    $this->processBooleans();
    $this->processID();
    $this->processClass();
    $this->processData();
    $this->processGrid();
    $this->processSetting();
    $this->processTag();
    $this->processInlineStyle();


    // Sub class can override this to provide
    // their own attributes preprocess function.
    $this->processCustomRules();

    $this->atts = apply_filters('vtcore_wordpress_shortcode_attributes_alter', $this->atts);

    return $this->atts;
  }


  public function extract() {
    return $this->atts;
  }


  public function set(array $value) {
    $this->atts = $value;
    return $this;
  }


  public function reset() {
    $this->atts = array();
    return $this;
  }


  public function add($keys, $value) {
    VTCore_Utility::setArrayValueKeys($this->atts, $keys, $value);
    return $this;
  }


  public function get($keys) {
    return VTCore_Utility::getArrayValueKeys($this->atts, $keys);
  }


  public function remove($keys) {
    VTCore_Utility::removeArrayValueKeys($this->atts, $keys);
    return $this;
  }


  public function merge(array $options) {
    $this->atts = VTCore_Utility::arrayMergeRecursiveDistinct($options, $this->atts);
    return $this;
  }


}