<?php
/**
 * Copyright (C) 2015-2016 Graham Breach
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * For more information, please contact <graham@goat1000.com>
 */

require_once "SVGGraphCoords.php";

define("SVGG_SHAPE_ABOVE", 1);
define("SVGG_SHAPE_BELOW", 0);

/**
 * Arbitrary shapes for adding to graphs
 */
class SVGGraphShapeList {

  private $graph;
  private $shapes = array();

  public function __construct(&$graph)
  {
    $this->graph = $graph;
  }

  /**
   * Load shapes from options list
   */
  public function Load(&$settings)
  {
    if(!isset($settings['shape']))
      return;
  
    if(!is_array($settings['shape']) || !isset($settings['shape'][0]))
      throw new Exception('Malformed shape option');

    if(!is_array($settings['shape'][0])) {
      $this->AddShape($settings['shape']);
    } else {
      foreach($settings['shape'] as $shape) {
        $this->AddShape($shape);
      }
    }
  }

  /**
   * Draw all the shapes for the selected depth
   */
  public function Draw($depth)
  {
    $content = array();
    foreach($this->shapes as $shape) {
      if($shape->Depth($depth))
        $content[] = $shape->Draw($this->graph);
    }
    return implode($content);
  }

  /**
   * Adds a shape from config array
   */
  private function AddShape(&$shape_array)
  {
    $shape = $shape_array[0];
    unset($shape_array[0]);

    $class_map = array(
      'circle' => 'SVGGraphCircle',
      'ellipse' => 'SVGGraphEllipse',
      'rect' => 'SVGGraphRect',
      'line' => 'SVGGraphLine',
      'polyline' => 'SVGGraphPolyLine',
      'polygon' => 'SVGGraphPolygon',
      'path' => 'SVGGraphPath',
    );

    if(isset($class_map[$shape]) && class_exists($class_map[$shape])) {
      $depth = SVGG_SHAPE_BELOW;
      if(isset($shape_array['depth'])) {
        if($shape_array['depth'] == 'above')
          $depth = SVGG_SHAPE_ABOVE;
      }
      if(isset($shape_array['clip_to_grid']) && $shape_array['clip_to_grid'] &&
        method_exists($this->graph, 'GridClipPath')) {
        $clip_id = $this->graph->GridClipPath();
        $shape_array['clip-path'] = "url(#{$clip_id})";
      }
      unset($shape_array['depth'], $shape_array['clip_to_grid']);
      $this->shapes[] = new $class_map[$shape]($shape_array, $depth);
    } else {
      throw new Exception("Unknown shape [{$shape}]");
    }
  }
}

abstract class SVGGraphShape {

  protected $depth = SVGG_SHAPE_BELOW;
  protected $element = '';
  protected $link = NULL;
  protected $link_target = '_blank';
  protected $coords = NULL;

  /**
   * attributes required to draw shape
   */
  protected $required = array();

  /**
   * attributes that support coordinate transformation
   */
  protected $transform = array();

  /**
   * coordinate pairs for dependent transforns - don't include them in
   * $transform or they will be transformed twice
   */
  protected $transform_pairs = array();

  /**
   * colour gradients/patterns, and whether to allow gradients
   */
  private $colour_convert = array(
    'stroke' => true,
    'fill' => false
  );

  /**
   * default attributes for all shapes
   */
  protected $attrs = array(
    'stroke' => '#000',
    'fill' => 'none'
  );

  public function __construct(&$attrs, $depth)
  {
    $this->attrs = array_merge($this->attrs, $attrs);
    $this->depth = $depth;

    $missing = array();
    foreach($this->required as $opt)
      if(!isset($this->attrs[$opt]))
        $missing[] = $opt;

    if(count($missing))
      throw new Exception("{$this->element} attribute(s) not found: " .
        implode(', ', $missing));

    if(isset($this->attrs['href']))
      $this->link = $this->attrs['href'];
    if(isset($this->attrs['xlink:href']))
      $this->link = $this->attrs['xlink:href'];
    if(isset($this->attrs['target']))
      $this->link_target = $this->attrs['target'];
    unset($this->attrs['href'], $this->attrs['xlink:href'],
      $this->attrs['target']);
  }

  /**
   * returns true if the depth is correct
   */
  public function Depth($d)
  {
    return $this->depth == $d;
  }

  /**
   * draws the shape
   */
  public function Draw(&$graph)
  {
    $this->coords = new SVGGraphCoords($graph);

    $attributes = array();
    foreach($this->attrs as $attr => $value) {
      if(!is_null($value)) {
        if(isset($this->transform[$attr])) {
          $val = $this->coords->Transform($value, $this->transform[$attr]);
        } else {
          $val = isset($this->colour_convert[$attr]) ? 
            $graph->ParseColour($value, NULL, $this->colour_convert[$attr]) :
            $value;
        }
        $attr = str_replace('_', '-', $attr);
        $attributes[$attr] = $val;
      }
    }
    $this->TransformCoordinates($attributes);
    $element = $this->DrawElement($graph, $attributes);
    if(!is_null($this->link)) {
      $link = array('xlink:href' => $this->link);
      if(!is_null($this->link_target))
        $link['target'] = $this->link_target;
      $element = $graph->Element('a', $link, NULL, $element);
    }
    return $element;
  }

  /**
   * Transform coordinate pairs
   */
  protected function TransformCoordinates(&$attributes)
  {
    if(count($this->transform_pairs)) {
      foreach($this->transform_pairs as $pair) {
        $coords = $this->coords->TransformCoords($attributes[$pair[0]],
          $attributes[$pair[1]]);
        $attributes[$pair[0]] = $coords[0];
        $attributes[$pair[1]] = $coords[1];
      }
    }
  }

  /**
   * Performs the conversion to SVG fragment
   */
  protected function DrawElement(&$graph, &$attributes)
  {
    return $graph->Element($this->element, $attributes);
  }

}

class SVGGraphCircle extends SVGGraphShape {
  protected $element = 'circle';
  protected $required = array('cx','cy','r');
  protected $transform = array('r' => 'y');
  protected $transform_pairs = array(array('cx', 'cy'));
}

class SVGGraphEllipse extends SVGGraphShape {
  protected $element = 'ellipse';
  protected $required = array('cx','cy','rx','ry');
  protected $transform = array('rx' => 'x', 'ry' => 'y');
  protected $transform_pairs = array(array('cx', 'cy'));
}

class SVGGraphRect extends SVGGraphShape {
  protected $element = 'rect';
  protected $required = array('x','y','width','height');
  protected $transform = array('width' => 'x', 'height' => 'y');
  protected $transform_pairs = array(array('x', 'y'));
}

class SVGGraphLine extends SVGGraphShape {
  protected $element = 'line';
  protected $required = array('x1','y1','x2','y2');
  protected $transform_pairs = array(array('x1', 'y1'), array('x2','y2'));
}

class SVGGraphPath extends SVGGraphShape {
  protected $element = 'path';
  protected $required = array('d');
}

class SVGGraphPolyLine extends SVGGraphShape {
  protected $element = 'polyline';
  protected $required = array('points');

  public function __construct(&$attrs, $depth)
  {
    parent::__construct($attrs, $depth);
    if(!is_array($this->attrs['points']))
      $this->attrs['points'] = explode(' ', $this->attrs['points']);
    $count = count($this->attrs['points']);
    if($count < 4 || $count % 2 == 1)
      throw new Exception("Shape must have at least 2 pairs of points");
  }

  /**
   * Override to transform pairs of points
   */
  protected function TransformCoordinates(&$attributes)
  {
    $count = count($attributes['points']);
    for($i = 0; $i < $count; $i += 2) {
      $x = $attributes['points'][$i];
      $y = $attributes['points'][$i + 1];
      $coords = $this->coords->TransformCoords($x, $y);
      $attributes['points'][$i] = $coords[0];
      $attributes['points'][$i + 1] = $coords[1];
    }
  }

  /**
   * Override to build the points attribute
   */
  protected function DrawElement(&$graph, &$attributes)
  {
    $attributes['points'] = implode(' ', $attributes['points']);
    return parent::DrawElement($graph, $attributes);
  }
}

class SVGGraphPolygon extends SVGGraphPolyLine {
  protected $element = 'polygon';
}