<?php

/*
 * This file is part of the Ivory Google Map package.
 *
 * (c) Eric GELOEN <geloen.eric@gmail.com>
 *
 * For the full copyright and license information, please read the LICENSE
 * file that was distributed with this source code.
 */

namespace Ivory\GoogleMap;

use Ivory\GoogleMap\Assets\AbstractJavascriptVariableAsset;
use Ivory\GoogleMap\Base\Coordinate;
use Ivory\GoogleMap\Base\Bound;
use Ivory\GoogleMap\Controls\MapTypeControl;
use Ivory\GoogleMap\Controls\OverviewMapControl;
use Ivory\GoogleMap\Controls\PanControl;
use Ivory\GoogleMap\Controls\RotateControl;
use Ivory\GoogleMap\Controls\ScaleControl;
use Ivory\GoogleMap\Controls\StreetViewControl;
use Ivory\GoogleMap\Controls\ZoomControl;
use Ivory\GoogleMap\Events\EventManager;
use Ivory\GoogleMap\Exception\MapException;
use Ivory\GoogleMap\Layers\KMLLayer;
use Ivory\GoogleMap\Overlays\Circle;
use Ivory\GoogleMap\Overlays\EncodedPolyline;
use Ivory\GoogleMap\Overlays\GroundOverlay;
use Ivory\GoogleMap\Overlays\InfoWindow;
use Ivory\GoogleMap\Overlays\Marker;
use Ivory\GoogleMap\Overlays\MarkerCluster;
use Ivory\GoogleMap\Overlays\Polygon;
use Ivory\GoogleMap\Overlays\Polyline;
use Ivory\GoogleMap\Overlays\Rectangle;

/**
 * Map wich describes a google map.
 *
 * @see http://code.google.com/apis/maps/documentation/javascript/reference.html#Map
 * @author GeLo <geloen.eric@gmail.com>
 */
class Map extends AbstractJavascriptVariableAsset
{
    /** @var string */
    protected $htmlContainerId;

    /** @var boolean */
    protected $async;

    /** @var boolean */
    protected $autoZoom;

    /** @var \Ivory\GoogleMap\Base\Coordinate */
    protected $center;

    /** @var \Ivory\GoogleMap\Base\Bound */
    protected $bound;

    /** @var array */
    protected $mapOptions;

    /** @var array */
    protected $stylesheetOptions;

    /** @var \Ivory\GoogleMap\Controls\MapTypeControl */
    protected $mapTypeControl;

    /** @var \Ivory\GoogleMap\Controls\OverviewMapControl */
    protected $overviewMapControl;

    /** @var \Ivory\GoogleMap\Controls\PanControl */
    protected $panControl;

    /** @var \Ivory\GoogleMap\Controls\RotateControl */
    protected $rotateControl;

    /** @var \Ivory\GoogleMap\Controls\ScaleControl */
    protected $scaleControl;

    /** @var \Ivory\GoogleMap\Controls\StreetViewControl */
    protected $streetViewControl;

    /** @var \Ivory\GoogleMap\Controls\ZoomControl */
    protected $zoomControl;

    /** @var \Ivory\GoogleMap\Events\EventManager */
    protected $eventManager;

    /** @var \Ivory\GoogleMap\Overlays\MarkerCluster */
    protected $markerCluster;

    /** @var array */
    protected $infoWindows;

    /** @var array */
    protected $polylines;

    /** @var array */
    protected $encodedPolylines;

    /** @var array */
    protected $polygons;

    /** @var array */
    protected $rectangles;

    /** @var array */
    protected $circles;

    /** @var array */
    protected $groundOverlays;

    /** @var array */
    protected $kmlLayers;

    /** @var array */
    protected $libraries;

    /** @var string */
    protected $language;

    /** @var string */
    protected $apiKey;

    /**
     * Creates a map.
     */
    public function __construct()
    {
        $this->setPrefixJavascriptVariable('map_');

        $this->htmlContainerId = 'map_canvas';
        $this->async = false;
        $this->autoZoom = false;

        $this->center = new Coordinate();
        $this->bound = new Bound();
        $this->eventManager = new EventManager();

        $this->mapOptions = array(
            'mapTypeId' => MapTypeId::ROADMAP,
            'zoom'      => 5,
        );

        $this->stylesheetOptions = array(
            'width'  => '100%',
            'height' => '100%',
        );

        $this->markerCluster = new MarkerCluster();

        $this->infoWindows = array();
        $this->polylines = array();
        $this->encodedPolylines = array();
        $this->polygons = array();
        $this->rectangles = array();
        $this->circles = array();
        $this->groundOverlays = array();
        $this->kmlLayers = array();

        $this->libraries = array();
        $this->language = 'en';
        $this->apiKey = null;
    }

    /**
     * Gets the map HTML container ID.
     *
     * @return string The map HTML constainer ID.
     */
    public function getHtmlContainerId()
    {
        return $this->htmlContainerId;
    }

    /**
     * Sets the map HTML container ID.
     *
     * @param string $htmlContainerId The map HTML container ID.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the HTML container ID is not a string.
     */
    public function setHtmlContainerId($htmlContainerId)
    {
        if (!is_string($htmlContainerId)) {
            throw MapException::invalidHtmlContainerId();
        }

        $this->htmlContainerId = $htmlContainerId;
    }

    /**
     * Check if the map loading is asynchronous.
     *
     * @return boolean TRUE if the map loading is asynchronous else FALSE.
     */
    public function isAsync()
    {
        return $this->async;
    }

    /**
     * Sets if the map loading is asynchronous.
     *
     * @param boolean $async TRUE if the map loading is asynchronous else FALSE.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the async flag is not a boolean.
     */
    public function setAsync($async)
    {
        if (!is_bool($async)) {
            throw MapException::invalidAsync();
        }

        $this->async = $async;
    }

    /**
     * Check if the map autozooms.
     *
     * @return boolean TRUE if the map autozooms else FALSE.
     */
    public function isAutoZoom()
    {
        return $this->autoZoom;
    }

    /**
     * Sets if the map autozooms.
     *
     * @param boolean $autoZoom TRUE if the map autozooms else FALSE.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the auto zoom flag is not a boolean.
     */
    public function setAutoZoom($autoZoom)
    {
        if (!is_bool($autoZoom)) {
            throw MapException::invalidAutoZoom();
        }

        $this->autoZoom = $autoZoom;
    }

    /**
     * Gets the map center.
     *
     * @return \Ivory\GoogleMap\Base\Coordinate The map center.
     */
    public function getCenter()
    {
        return $this->center;
    }

    /**
     * Sets the map center.
     *
     * Available prototypes:
     *  - function setCenter(Ivory\GoogleMap\Base\Coordinate $center)
     *  - function setCenter(double $latitude, double $longitude, boolean $noWrap = true)
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the center is not valid (prototypes).
     */
    public function setCenter()
    {
        $args = func_get_args();

        if (isset($args[0]) && ($args[0] instanceof Coordinate)) {
            $this->center = $args[0];
        } elseif ((isset($args[0]) && is_numeric($args[0])) && (isset($args[1]) && is_numeric($args[1]))) {
            $this->center->setLatitude($args[0]);
            $this->center->setLongitude($args[1]);

            if (isset($args[2]) && is_bool($args[2])) {
                $this->center->setNoWrap($args[2]);
            }
        } else {
            throw MapException::invalidCenter();
        }
    }

    /**
     * Gets the map bound.
     *
     * @return \Ivory\GoogleMap\Base\Bound The map bound.
     */
    public function getBound()
    {
        return $this->bound;
    }

    /**
     * Sets the map bound.
     *
     * Available prototypes:
     *  - function setBound(Ivory\GoogleMap\Base\Bound $bound = null)
     *  - function setBount(Ivory\GoogleMap\Base\Coordinate $southWest, Ivory\GoogleMap\Base\Coordinate $northEast)
     *  - function setBound(
     *      double $southWestLatitude,
     *      double $southWestLongitude,
     *      double $northEastLatitude,
     *      double $northEastLongitude,
     *      boolean southWestNoWrap = true,
     *      boolean $northEastNoWrap = true
     *  )
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the bound is not valid (prototypes).
     */
    public function setBound()
    {
        $args = func_get_args();

        if (isset($args[0]) && ($args[0] instanceof Bound)) {
            $this->bound = $args[0];
        } elseif ((isset($args[0]) && ($args[0] instanceof Coordinate))
            && (isset($args[1]) && ($args[1] instanceof Coordinate))
        ) {
            $this->bound->setSouthWest($args[0]);
            $this->bound->setNorthEast($args[1]);
        } elseif ((isset($args[0]) && is_numeric($args[0]))
            && (isset($args[1]) && is_numeric($args[1]))
            && (isset($args[2]) && is_numeric($args[2]))
            && (isset($args[3]) && is_numeric($args[3]))
        ) {
            $this->bound->setSouthWest(new Coordinate($args[0], $args[1]));
            $this->bound->setNorthEast(new Coordinate($args[2], $args[3]));

            if (isset($args[4]) && is_bool($args[4])) {
                $this->bound->getSouthWest()->setNoWrap($args[4]);
            }

            if (isset($args[5]) && is_bool($args[5])) {
                $this->bound->getNorthEast()->setNoWrap($args[5]);
            }
        } elseif (!isset($args[0])) {
            $this->bound->setSouthWest(null);
            $this->bound->setNorthEast(null);
        } else {
            throw MapException::invalidBound();
        }
    }

    /**
     * Checks if the map option exists.
     *
     * @param string $mapOption The map option.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the map option is not valid.
     *
     * @return boolean TRUE if the map option exists else FALSE.
     */
    public function hasMapOption($mapOption)
    {
        if (!is_string($mapOption)) {
            throw MapException::invalidMapOption();
        }

        return isset($this->mapOptions[$mapOption]);
    }

    /**
     * Gets the map options
     *
     * @return array The map options.
     */
    public function getMapOptions()
    {
        return $this->mapOptions;
    }

    /**
     * Sets the map options.
     *
     * @param array $mapOptions The map options.
     */
    public function setMapOptions(array $mapOptions)
    {
        foreach ($mapOptions as $mapOption => $value) {
            $this->setMapOption($mapOption, $value);
        }
    }

    /**
     * Gets a specific map option.
     *
     * @param string $mapOption The map option.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the map option does not exist.
     *
     * @return mixed The map option value.
     */
    public function getMapOption($mapOption)
    {
        if (!$this->hasMapOption($mapOption)) {
            throw MapException::mapOptionDoesNotExist($mapOption);
        }

        return $this->mapOptions[$mapOption];
    }

    /**
     * Sets a specific map option
     *
     * @param string $mapOption The map option.
     * @param mixed  $value     The map option value.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the map option is not valid.
     */
    public function setMapOption($mapOption, $value)
    {
        if (!is_string($mapOption)) {
            throw MapException::invalidMapOption();
        }

        $this->mapOptions[$mapOption] = $value;
    }

    /**
     * Removes a map option.
     *
     * @param string $mapOption The map option.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the map option does not exist.
     */
    public function removeMapOption($mapOption)
    {
        if (!$this->hasMapOption($mapOption)) {
            throw MapException::mapOptionDoesNotExist($mapOption);
        }

        unset($this->mapOptions[$mapOption]);
    }

    /**
     * Checks if the stylesheet option exists.
     *
     * @param string $stylesheetOption The stylesheet option.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the stylesheet option is not valid.
     *
     * @return boolean TRUE if the stylesheet option exists else FALSE.
     */
    public function hasStylesheetOption($stylesheetOption)
    {
        if (!is_string($stylesheetOption)) {
            throw MapException::invalidStylesheetOption();
        }

        return isset($this->stylesheetOptions[$stylesheetOption]);
    }

    /**
     * Gets the stylesheet options.
     *
     * @return array The stylesheet options.
     */
    public function getStylesheetOptions()
    {
        return $this->stylesheetOptions;
    }

    /**
     * Sets the stylesheet options.
     *
     * @param array $stylesheetOptions The stylesheet options.
     */
    public function setStylesheetOptions(array $stylesheetOptions)
    {
        foreach ($stylesheetOptions as $stylesheetOption => $value) {
            $this->setStylesheetOption($stylesheetOption, $value);
        }
    }

    /**
     * Gets a specific stylesheet option.
     *
     * @param string $stylesheetOption  The stylesheet option.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the stylesheet option does not exist.
     *
     * @return mixed The stylesheet option value.
     */
    public function getStylesheetOption($stylesheetOption)
    {
        if (!$this->hasStylesheetOption($stylesheetOption)) {
            throw MapException::stylesheetOptionDoesNotExist($stylesheetOption);
        }

        return $this->stylesheetOptions[$stylesheetOption];
    }

    /**
     * Sets a specific stylesheet option.
     *
     * @param string $stylesheetOption The stylesheet option.
     * @param mixed  $value            The stylesheet option value.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the stylesheet option is not valid.
     */
    public function setStylesheetOption($stylesheetOption, $value)
    {
        if (!is_string($stylesheetOption)) {
            throw MapException::invalidStylesheetOption();
        }

        $this->stylesheetOptions[$stylesheetOption] = $value;
    }

    /**
     * Removes a stylesheet option.
     *
     * @param string $stylesheetOption The stylesheet option.
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the stylesheet option does not exist.
     */
    public function removeStylesheetOption($stylesheetOption)
    {
        if (!$this->hasStylesheetOption($stylesheetOption)) {
            throw MapException::stylesheetOptionDoesNotExist($stylesheetOption);
        }

        unset($this->stylesheetOptions[$stylesheetOption]);
    }

    /**
     * Checks if the map has a map type control.
     *
     * @return boolean TRUE if the map has a map type control else FALSE.
     */
    public function hasMapTypeControl()
    {
        return $this->mapTypeControl !== null;
    }

    /**
     * Gets the map type control.
     *
     * @return \Ivory\GoogleMap\Controls\MapTypeControl The map type control.
     */
    public function getMapTypeControl()
    {
        return $this->mapTypeControl;
    }

    /**
     * Sets the map type control.
     *
     * Available prototypes:
     *  - function setMapTypeControl(Ivory\GoogleMap\Controls\MapTypeControl $mapTypeControl = null)
     *  - function setMaptypeControl(array $mapTypeIds, string $controlPosition, string $mapTypeControlStyle)
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the map type control is not valid (prototypes).
     */
    public function setMapTypeControl()
    {
        $args = func_get_args();

        if (isset($args[0]) && ($args[0] instanceof MapTypeControl)) {
            $this->mapTypeControl = $args[0];
            $this->mapOptions['mapTypeControl'] = true;
        } elseif ((isset($args[0]) && is_array($args[0]))
            && (isset($args[1]) && is_string($args[1]))
            && (isset($args[2]) && is_string($args[2]))
        ) {
            if ($this->mapTypeControl === null) {
                $this->mapTypeControl = new MapTypeControl();
            }

            $this->mapTypeControl->setMapTypeIds($args[0]);
            $this->mapTypeControl->setControlPosition($args[1]);
            $this->mapTypeControl->setMapTypeControlStyle($args[2]);

            $this->mapOptions['mapTypeControl'] = true;
        } elseif (!isset($args[0])) {
            $this->mapTypeControl = null;

            if (isset($this->mapOptions['mapTypeControl'])) {
                unset($this->mapOptions['mapTypeControl']);
            }
        } else {
            throw MapException::invalidMapTypeControl();
        }
    }

    /**
     * Checks if the map has an overview map control.
     *
     * @return boolean TRUE if the map has an overview map control else FALSE.
     */
    public function hasOverviewMapControl()
    {
        return $this->overviewMapControl !== null;
    }

    /**
     * Gets the overview map control.
     *
     * @return \Ivory\GoogleMap\Controls\OverviewMapControl The overview map control.
     */
    public function getOverviewMapControl()
    {
        return $this->overviewMapControl;
    }

    /**
     * Sets the overview map control.
     *
     * Available prototypes:
     *  - function setOverviewMapControl(Ivory\GoogleMap\Controls\OverviewMapControl $overviewMapControl = null)
     *  - function setOverviewMapControl(boolean $opened)
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the overview map control is not valid (prototypes).
     */
    public function setOverviewMapControl()
    {
        $args = func_get_args();

        if (isset($args[0]) && ($args[0]) instanceof OverviewMapControl) {
            $this->overviewMapControl = $args[0];
            $this->mapOptions['overviewMapControl'] = true;
        } elseif (isset($args[0]) && is_bool($args[0])) {
            if ($this->overviewMapControl === null) {
                $this->overviewMapControl = new OverviewMapControl();
            }

            $this->overviewMapControl->setOpened($args[0]);
            $this->mapOptions['overviewMapControl'] = true;
        } elseif (!isset($args[0])) {
            $this->overviewMapControl = null;

            if (isset($this->mapOptions['overviewMapControl'])) {
                unset($this->mapOptions['overviewMapControl']);
            }
        } else {
            throw MapException::invalidOverviewMapControl();
        }
    }

    /**
     * Checks if the map has a pan control.
     *
     * @return boolean TRUE if the map has a pan control else FALSE.
     */
    public function hasPanControl()
    {
        return $this->panControl !== null;
    }

    /**
     * Gets the map pan control.
     *
     * @return \Ivory\GoogleMap\Controls\PanControl The map pan control.
     */
    public function getPanControl()
    {
        return $this->panControl;
    }

    /**
     * Sets the map pan control.
     *
     * Available prototypes:
     *  - function setPanControl(Ivory\GoogleMap\Controls\PanControl $panControl = null)
     *  - function setPanControl(string $controlPosition)
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the pan control is not valid (prototypes).
     */
    public function setPanControl()
    {
        $args = func_get_args();

        if (isset($args[0]) && ($args[0] instanceof PanControl)) {
            $this->panControl = $args[0];
            $this->mapOptions['panControl'] = true;
        } elseif (isset($args[0]) && is_string($args[0])) {
            if ($this->panControl === null) {
                $this->panControl = new PanControl();
            }

            $this->panControl->setControlPosition($args[0]);
            $this->mapOptions['panControl'] = true;
        } elseif (!isset($args[0])) {
            $this->panControl = null;

            if (isset($this->mapOptions['panControl'])) {
                unset($this->mapOptions['panControl']);
            }
        } else {
            throw MapException::invalidPanControl();
        }
    }

    /**
     * Checks if the map has a rotate control.
     *
     * @return boolean TRUE if the map has a rotate control else FALSE.
     */
    public function hasRotateControl()
    {
        return $this->rotateControl !== null;
    }

    /**
     * Gets the map rotate control.
     *
     * @return \Ivory\GoogleMap\Controls\RotateControl The map rotate control.
     */
    public function getRotateControl()
    {
        return $this->rotateControl;
    }

    /**
     * Sets the map rotate control.
     *
     * Available prototypes:
     *  - function setRotateControl(Ivory\GoogleMap\Controls\RotateControl $rotateControl = null)
     *  - function setRotateControl(string $controlPosition)
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the rotate control is not valid (prototypes).
     */
    public function setRotateControl()
    {
        $args = func_get_args();

        if (isset($args[0]) && ($args[0] instanceof RotateControl)) {
            $this->rotateControl = $args[0];
            $this->mapOptions['rotateControl'] = true;
        } elseif (isset($args[0]) && is_string($args[0])) {
            if ($this->rotateControl === null) {
                $this->rotateControl = new RotateControl();
            }

            $this->rotateControl->setControlPosition($args[0]);
            $this->mapOptions['rotateControl'] = true;
        } elseif (!isset($args[0])) {
            $this->rotateControl = null;

            if (isset($this->mapOptions['rotateControl'])) {
                unset($this->mapOptions['rotateControl']);
            }
        } else {
            throw MapException::invalidRotateControl();
        }
    }

    /**
     * Checks if the map has a scale control else FALSE.
     *
     * @return boolean TRUE if the map has a scale control else FALSE.
     */
    public function hasScaleControl()
    {
        return $this->scaleControl !== null;
    }

    /**
     * Gets the map scale control.
     *
     * @return \Ivory\GoogleMap\Controls\ScaleControl The map scale control.
     */
    public function getScaleControl()
    {
        return $this->scaleControl;
    }

    /**
     * Sets the map scale control.
     *
     * Available prototypes:
     *  - function setScaleControl(Ivory\GoogleMap\Controls\ScaleControl $scaleControl = null)
     *  - function setScaleControl(string $controlPosition, string $scaleControlStyle)
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the scale control is not valid (prototypes).
     */
    public function setScaleControl()
    {
        $args = func_get_args();

        if (isset($args[0]) && ($args[0] instanceof ScaleControl)) {
            $this->scaleControl = $args[0];
            $this->mapOptions['scaleControl'] = true;
        } elseif ((isset($args[0]) && is_string($args[0])) && (isset($args[1]) && is_string($args[1]))) {
            if ($this->scaleControl === null) {
                $this->scaleControl = new ScaleControl();
            }

            $this->scaleControl->setControlPosition($args[0]);
            $this->scaleControl->setScaleControlStyle($args[1]);

            $this->mapOptions['scaleControl'] = true;
        } elseif (!isset($args[0])) {
            $this->scaleControl = null;

            if (isset($this->mapOptions['scaleControl'])) {
                unset($this->mapOptions['scaleControl']);
            }
        } else {
            throw MapException::invalidScaleControl();
        }
    }

    /**
     * Checks if the map has a street view control.
     *
     * @return boolean TRUE if the map has a street view control else FALSE.
     */
    public function hasStreetViewControl()
    {
        return $this->streetViewControl !== null;
    }

    /**
     * Gets the map street view control.
     *
     * @return \Ivory\GoogleMap\Controls\StreetViewControl The map street view control.
     */
    public function getStreetViewControl()
    {
        return $this->streetViewControl;
    }

    /**
     * Sets the map street view control.
     *
     * Available prototypes:
     *  - function setStreetViewControl(Ivory\GoogleMap\Controls\StreetViewControl $streetViewControl = null)
     *  - function setStreetViewControl(string $controlPosition)
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the street view control is not valid (prototypes).
     */
    public function setStreetViewControl()
    {
        $args = func_get_args();

        if (isset($args[0]) && ($args[0] instanceof StreetViewControl)) {
            $this->streetViewControl = $args[0];
            $this->mapOptions['streetViewControl'] = true;
        } elseif (isset($args[0]) && is_string($args[0])) {
            if ($this->streetViewControl === null) {
                $this->streetViewControl = new StreetViewControl();
            }

            $this->streetViewControl->setControlPosition($args[0]);
            $this->mapOptions['streetViewControl'] = true;
        } elseif (!isset($args[0])) {
            $this->streetViewControl = null;

            if (isset($this->mapOptions['streetViewControl'])) {
                unset($this->mapOptions['streetViewControl']);
            }
        } else {
            throw MapException::invalidStreetViewControl();
        }
    }

    /**
     * Checks if the map has a zoom control.
     *
     * @return boolean TRUE if the map has a zoom control else FALSE.
     */
    public function hasZoomControl()
    {
        return $this->zoomControl !== null;
    }

    /**
     * Gets the map zoom control.
     *
     * @return \Ivory\GoogleMap\Controls\ZoomControl The map zoom control.
     */
    public function getZoomControl()
    {
        return $this->zoomControl;
    }

    /**
     * Sets the map zoom control.
     *
     * Available prototypes:
     *  - function setZoomControl(Ivory\GoogleMap\Controls\ZoomControl $zoomControl = null)
     *  - function setZoomControl(string $controlPosition, string $zoomControlStyle)
     *
     * @throws \Ivory\GoogleMap\Exception\MapException If the zoom control is not valid (prototypes).
     */
    public function setZoomControl()
    {
        $args = func_get_args();

        if (isset($args[0]) && ($args[0] instanceof ZoomControl)) {
            $this->zoomControl = $args[0];
            $this->mapOptions['zoomControl'] = true;
        } elseif ((isset($args[0]) && is_string($args[0])) && (isset($args[1]) && is_string($args[1]))) {
            if ($this->zoomControl === null) {
                $this->zoomControl = new ZoomControl();
            }

            $this->zoomControl->setControlPosition($args[0]);
            $this->zoomControl->setZoomControlStyle($args[1]);

            $this->mapOptions['zoomControl'] = true;
        } elseif (!isset($args[0])) {
            $this->zoomControl = null;

            if (isset($this->mapOptions['zoomControl'])) {
                unset($this->mapOptions['zoomControl']);
            }
        } else {
            throw MapException::invalidZoomControl();
        }
    }

    /**
     * Gets the map event manager.
     *
     * @return \Ivory\GoogleMap\Events\EventManager The map event manager.
     */
    public function getEventManager()
    {
        return $this->eventManager;
    }

    /**
     * Sets the map event manager.
     *
     * @param \Ivory\GoogleMap\Events\EventManager $eventManager The map event manager.
     */
    public function setEventManager(EventManager $eventManager)
    {
        $this->eventManager = $eventManager;
    }

    /**
     * Gets the marker cluster.
     *
     * @return \Ivory\GoogleMap\Overlays\MarkerCluster The marker cluster.
     */
    public function getMarkerCluster()
    {
        return $this->markerCluster;
    }

    /**
     * Sets the marker cluster.
     *
     * @param \Ivory\GoogleMap\Overlays\MarkerCluster $markerCluster The marker cluster.
     */
    public function setMarkerCluster(MarkerCluster $markerCluster)
    {
        $this->markerCluster = $markerCluster;
    }

    /**
     * Gets the map markers.
     *
     * @return array The map markers.
     */
    public function getMarkers()
    {
        return $this->markerCluster->getMarkers();
    }

    /**
     * Add a map marker.
     *
     * @param \Ivory\GoogleMap\Overlays\Marker $marker The marker to add.
     */
    public function addMarker(Marker $marker)
    {
        $this->markerCluster->addMarker($marker);

        if ($this->autoZoom) {
            $this->bound->extend($marker);
        }
    }

    /**
     * Gets the map info windows.
     *
     * @return array The map info windows.
     */
    public function getInfoWindows()
    {
        return $this->infoWindows;
    }

    /**
     * Add a map info window.
     *
     * @param \Ivory\GoogleMap\Overlays\InfoWindow $infoWindow The info window to add.
     */
    public function addInfoWindow(InfoWindow $infoWindow)
    {
        $this->infoWindows[] = $infoWindow;

        if ($this->autoZoom) {
            $this->bound->extend($infoWindow);
        }
    }

    /**
     * Gets the map polylines.
     *
     * @return array The map polylines.
     */
    public function getPolylines()
    {
        return $this->polylines;
    }

    /**
     * Add a map polyline.
     *
     * @param \Ivory\GoogleMap\Overlays\Polyline The polyline to add.
     */
    public function addPolyline(Polyline $polyline)
    {
        $this->polylines[] = $polyline;

        if ($this->autoZoom) {
            $this->bound->extend($polyline);
        }
    }

    /**
     * Gets the map encoded polylines.
     *
     * @return array The map encoded polylines.
     */
    public function getEncodedPolylines()
    {
        return $this->encodedPolylines;
    }

    /**
     * Adds an encoded polyline to the map.
     *
     * @param \Ivory\GoogleMap\Overlays\EncodedPolyline $encodedPolyline The encoded polyline to add.
     */
    public function addEncodedPolyline(EncodedPolyline $encodedPolyline)
    {
        $this->encodedPolylines[] = $encodedPolyline;

        if ($this->autoZoom) {
            $this->bound->extend($encodedPolyline);
        }
    }

    /**
     * Gets the map polygons.
     *
     * @return array The map polygons.
     */
    public function getPolygons()
    {
        return $this->polygons;
    }

    /**
     * Add a map polygon.
     *
     * @param \Ivory\GoogleMap\Overlays\Polygon $polygon The polygon to add.
     */
    public function addPolygon(Polygon $polygon)
    {
        $this->polygons[] = $polygon;

        if ($this->autoZoom) {
            $this->bound->extend($polygon);
        }
    }

    /**
     * Gets the map rectangles.
     *
     * @return array The map rectangles.
     */
    public function getRectangles()
    {
        return $this->rectangles;
    }

    /**
     * Add a map rectangle to the map.
     *
     * @param \Ivory\GoogleMap\Overlays\Rectangle $rectangle The rectangle to add.
     */
    public function addRectangle(Rectangle $rectangle)
    {
        $this->rectangles[] = $rectangle;

        if ($this->autoZoom) {
            $this->bound->extend($rectangle);
        }
    }

    /**
     * Gets the map circles
     *
     * @return array The map circles.
     */
    public function getCircles()
    {
        return $this->circles;
    }

    /**
     * Add a circle to the map.
     *
     * @param \Ivory\GoogleMap\Overlays\Circle $circle The circle to add.
     */
    public function addCircle(Circle $circle)
    {
        $this->circles[] = $circle;

        if ($this->autoZoom) {
            $this->bound->extend($circle);
        }
    }

    /**
     * Gets the map ground overlays.
     *
     * @return array The map ground overlays.
     */
    public function getGroundOverlays()
    {
        return $this->groundOverlays;
    }

    /**
     * Add a ground overlay to the map.
     *
     * @param \Ivory\GoogleMapBundle\Model\Overlays\GroupOverlay $groundOverlay The ground overlay to add.
     */
    public function addGroundOverlay(GroundOverlay $groundOverlay)
    {
        $this->groundOverlays[] = $groundOverlay;

        if ($this->autoZoom) {
            $this->bound->extend($groundOverlay);
        }
    }

    /**
     * Gets the KML layers.
     *
     * @return array The KML layers.
     */
    public function getKMLLayers()
    {
        return $this->kmlLayers;
    }

    /**
     * Adds a KML Layer to the map.
     *
     * @param \Ivory\GoogleMap\Layers\KMLLayer $kmlLayer The KML Layer to add.
     */
    public function addKMLLayer(KMLLayer $kmlLayer)
    {
        $this->kmlLayers[] = $kmlLayer;
    }

    /**
     * Checks if the map has libraries.
     *
     * @return boolean TRUE if the map has libraries else FALSE.
     */
    public function hasLibraries()
    {
        return !empty($this->libraries);
    }

    /**
     * Gets the map libraries.
     *
     * @return array The map libraries.
     */
    public function getLibraries()
    {
        return $this->libraries;
    }

    /**
     * Sets the map libraries.
     *
     * @param array $libraries The map libraries.
     */
    public function setLibraries(array $libraries)
    {
        $this->libraries = $libraries;
    }

    /**
     * Gets the map language.
     *
     * @return string The map language.
     */
    public function getLanguage()
    {
        return $this->language;
    }

    /**
     * Sets the map langauge.
     *
     * @param string $language The map langauge.
     */
    public function setLanguage($language)
    {
        $this->language = $language;
    }

    /**
     * Gets the browser API key
     *
     * @return string The browser API key
     */
    public function getApiKey()
    {
        return $this->apiKey;
    }

    /**
     * Sets the browser API key
     *
     * @param string $apiKey The Browser API key
     */
    public function setApiKey($apiKey)
    {
        $this->apiKey = $apiKey;
    }
}


<?php
namespace Perry\Cache\File;

use \DateTime;
use Psr\Cache\ItemInterface;

class FileItem implements ItemInterface
{
    /**
     * @var string
     */
    private $key;

    /**
     * @var \Serializable
     */
    private $value = null;

    /**
     * @var int
     */
    private $ttl = 0;

    /**
     * @var \DateTime
     */
    private $updated;

    /**
     * @var bool
     */
    private $hit = false;

    /**
     * @var string
     */
    private $filename;

    /**
     * Returns the key for the current cache item.
     *
     * The key is loaded by the Implementing Library, but should be available to
     * the higher level callers when needed.
     *
     * @return string
     *   The key string for this cache item.
     */
    public function getKey()
    {
        return $this->key;
    }

    /**
     * Retrieves the value of the item from the cache associated with this objects key.
     *
     * The value returned must be identical to the value original stored by set().
     *
     * if isHit() returns false, this method MUST return null. Note that null
     * is a legitimate cached value, so the isHit() method SHOULD be used to
     * differentiate between "null value was found" and "no value was found."
     *
     * @return \Serializable|null
     *   The value corresponding to this cache item's key, or null if not found.
     */
    public function get()
    {
        // we hit the cache when we get the item.. so we allways return the value
        return $this->value;
    }

    /**
     * Stores a value into the cache.
     *
     * The $value argument may be any item that can be serialized by PHP,
     * although the method of serialization is left up to the Implementing
     * Library.
     *
     * Implementing Libraries MAY provide a default TTL if one is not specified.
     * If no TTL is specified and no default TTL has been set, the TTL MUST
     * be set to the maximum possible duration of the underlying storage
     * mechanism, or permanent if possible.
     *
     * @param mixed $value
     *   The serializable value to be stored.
     * @param int|DateTime $ttl
     *   - If an integer is passed, it is interpreted as the number of seconds
     *     after which the item MUST be considered expired.
     *   - If a DateTime object is passed, it is interpreted as the point in
     *     time after which the the item MUST be considered expired.
     *   - If no value is passed, a default value MAY be used. If none is set,
     *     the value should be stored permanently or for as long as the
     *     implementation allows.
     * @return bool
     *   Returns true if the item was successfully saved, or false if there was
     *   an error.
     */
    public function set($value = null, $ttl = null)
    {
        $now = new DateTime();

        // whoever came up with the idea of allowing int AND datetime...
        // so lets convert this to an int (its a time to live, not a date to delete ffs)
        if ($ttl instanceof DateTime) {
            $ttl = $ttl->getTimestamp() - $now->getTimestamp();
        }

        $data = [
            "updated" => $now,
            "value" => $value,
            "key" => $this->key,
            "ttl" => $ttl
        ];

        return false !== file_put_contents($this->filename, serialize($data));
    }

    /**
     * Confirms if the cache item lookup resulted in a cache hit.
     *
     * Note: This method MUST NOT have a race condition between calling isHit()
     * and calling get().
     *
     * @return bool
     *   True if the request resulted in a cache hit.  False otherwise.
     */
    public function isHit()
    {
        return $this->hit;
    }

    /**
     * Removes the current key from the cache.
     *
     * @return \Psr\Cache\ItemInterface
     *   The current item.
     */
    public function delete()
    {
        if (file_exists($this->filename)) {
            unlink($this->filename);
        }
        $this->hit = false;
        $this->value = null;
        return $this;
    }

    /**
     * Confirms if the cache item exists in the cache.
     *
     * Note: This method MAY avoid retrieving the cached value for performance
     * reasons, which could result in a race condition between exists() and get().
     *
     * @return bool
     *  True if item exists in the cache, false otherwise.
     */
    public function exists()
    {
        // for us its the same
        return $this->isHit();
    }

    /**
     * @param string $filename
     * @param string $key
     * @return FileItem
     */
    public static function fromFile($filename, $key)
    {
        $instance = new self();

        // those are values we already know
        $instance->filename = $filename;
        $instance->key = $key;

        if (file_exists($filename)) {

            // fopen/fread/fclose since file_get_contents seems to not close the files properly
            $fp = fopen($filename, "r");

            // if we for whatever reason can't open this file,
            // we have a cache miss
            if (false === $fp) {
                $instance->hit =false;
                return $instance;
            }

            $contents ="";
            while (!feof($fp)) {
              $contents .= fread($fp, 8192);
            }
            fclose($fp);

            $data = unserialize($contents);

            $now = new \DateTime();

            /**
             * @var \DateTime $updated
             */
            $updated = $data['updated'];

            if ($updated->getTimestamp() + $data['ttl'] >= $now->getTimestamp()) {
                $instance->value = $data['value'];
                $instance->ttl = $data['ttl'];
                $instance->updated = $data['updated'];
                $instance->hit = true;
            }
        }
        return $instance;
    }
}


<?php

/*
 * This file is part of the Ivory Google Map package.
 *
 * (c) Eric GELOEN <geloen.eric@gmail.com>
 *
 * For the full copyright and license information, please read the LICENSE
 * file that was distributed with this source code.
 */

namespace Ivory\GoogleMap\Services\Directions;

use \DateTime;
use Ivory\GoogleMap\Base\Coordinate;
use Ivory\GoogleMap\Exception\DirectionsException;
use Ivory\GoogleMap\Services\Base\TravelMode;
use Ivory\GoogleMap\Services\Base\UnitSystem;

/**
 * Directions request represents a google map directions request.
 *
 * @see http://code.google.com/apis/maps/documentation/javascript/reference.html#DirectionsRequest
 * @author GeLo <geloen.eric@gmail.com>
 */
class DirectionsRequest
{
    /** @var boolean */
    protected $avoidHighways;

    /** @var boolean */
    protected $avoidTolls;

    /** @var string | \Ivory\GoogleMap\Base\Coordinate */
    protected $destination;

    /** @var boolean */
    protected $optimizeWaypoints;

    /** @var string | \Ivory\GoogleMap\Base\Coordinate */
    protected $origin;

    /** @var \DateTime */
    protected $departureTime;

    /** @var  \DateTime */
    protected $arrivalTime;

    /** @var boolean */
    protected $provideRouteAlternatives;

    /** @var string */
    protected $region;

    /** @var string */
    protected $language;

    /** @var string */
    protected $travelMode;

    /** @var string */
    protected $unitSystem;

    /** @var array */
    protected $waypoints;

    /** @var boolean */
    protected $sensor;

    /**
     * Creates a directions request.
     */
    public function __construct()
    {
        $this->waypoints = array();
        $this->sensor = false;
    }

    /**
     * Checks if the directions request has an avoid hightways flag.
     *
     * @return boolean TRUE if the directions request has an avoid hightways flag else FALSE.
     */
    public function hasAvoidHighways()
    {
        return $this->avoidHighways !== null;
    }

    /**
     * Checks if the directions request avoid hightways.
     *
     * @return boolean TRUE if the directions request avoids hightways else FALSE.
     */
    public function getAvoidHighways()
    {
        return $this->avoidHighways;
    }

    /**
     * Sets if the the directions request avoids hightways.
     *
     * @param boolean $avoidHighways TRUE if the directions request avoids hightways else FALSE.
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the avoid highways flag is not valid.
     */
    public function setAvoidHighways($avoidHighways = null)
    {
        if (!is_bool($avoidHighways) && ($avoidHighways !== null)) {
            throw DirectionsException::invalidDirectionsRequestAvoidHighways();
        }

        $this->avoidHighways = $avoidHighways;
    }

    /**
     * Checks if the directions request has an avoid tolls flag.
     *
     * @return boolean TRUE if the directions request has an avoid tolls flag else FALSE.
     */
    public function hasAvoidTolls()
    {
        return $this->avoidTolls !== null;
    }

    /**
     * Checks if the directions request avoid tolls.
     *
     * @return boolean TRUE if the directions request avoids tolls else FALSE.
     */
    public function getAvoidTolls()
    {
        return $this->avoidTolls;
    }

    /**
     * Sets if the the directions request avoids tolls.
     *
     * @param boolean $avoidTolls TRUE if the directions request avoids tolls else FALSE.
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the avoid tolls flag is not valid.
     */
    public function setAvoidTolls($avoidTolls = null)
    {
        if (!is_bool($avoidTolls) && ($avoidTolls !== null)) {
            throw DirectionsException::invalidDirectionsRequestAvoidTolls();
        }

        $this->avoidTolls = $avoidTolls;
    }

    /**
     * Checks if the directions request has a destination.
     *
     * @return boolean TRUE if the directions request has a destination else FALSE.
     */
    public function hasDestination()
    {
        return $this->destination !== null;
    }

    /**
     * Gets the directions request destination.
     *
     * @return string | \Ivory\GoogleMap\Base\Coordinate The directions request destination.
     */
    public function getDestination()
    {
        return $this->destination;
    }

    /**
     * Sets the directions request destination.
     *
     * Available prototypes:
     * - function setDestination(string $destination)
     * - function setDestination(Ivory\GoogleMap\Base\Coordinate $destination)
     * - function setDestination(double $latitude, double $longitude, boolean $noWrap)
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the destination is not valid (prototypes).
     */
    public function setDestination()
    {
        $args = func_get_args();

        if (isset($args[0]) && is_string($args[0])) {
            $this->destination = $args[0];
        } elseif (isset($args[0]) && ($args[0] instanceof Coordinate)) {
            $this->destination = $args[0];
        } elseif ((isset($args[0]) && is_numeric($args[0])) && (isset($args[1]) && is_numeric($args[1]))) {
            if ($this->destination === null) {
                $this->destination = new Coordinate();
            }

            $this->destination->setLatitude($args[0]);
            $this->destination->setLongitude($args[1]);

            if (isset($args[2]) && is_bool($args[2])) {
                $this->destination->setNoWrap($args[2]);
            }
        } else {
            throw DirectionsException::invalidDirectionsRequestDestination();
        }
    }

    /**
     * Checks if the directions request has the optimize waypoints flag.
     *
     * @return boolean TRUE if the directions request has the optimize waypoints flag else FALSE.
     */
    public function hasOptimizeWaypoints()
    {
        return $this->optimizeWaypoints !== null;
    }

    /**
     * Checks if the directions request optimizes waypoints.
     *
     * @return boolean TRUE if the directions request optmizes waypoints else FALSE.
     */
    public function getOptimizeWaypoints()
    {
        return $this->optimizeWaypoints;
    }

    /**
     * Sets if the directions request optimizes waypoints.
     *
     * @param boolean $optimizeWaypoints TRUE if the directions request optimizes waypoints else FALSE.
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the optimize waypoints flag is not valid.
     */
    public function setOptimizeWaypoints($optimizeWaypoints = null)
    {
        if (!is_bool($optimizeWaypoints) && ($optimizeWaypoints !== null)) {
            throw DirectionsException::invalidDirectionsRequestOptimizeWaypoints();
        }

        $this->optimizeWaypoints = $optimizeWaypoints;
    }

    /**
     * Checks if the directions request has an origin.
     *
     * @return boolean TRUE if the directions request has an origin else FALSE.
     */
    public function hasOrigin()
    {
        return $this->origin !== null;
    }

    /**
     * Gets the directions request origin.
     *
     * @return string | \Ivory\GoogleMap\Base\Coordinate The directions request origin.
     */
    public function getOrigin()
    {
        return $this->origin;
    }

    /**
     * Sets the directions request origin.
     *
     * Available prototypes:
     * - function setOrigin(string $destination)
     * - function setOrigin(Ivory\GoogleMap\Base\Coordinate $destination)
     * - function setOrigin(double $latitude, double $longitude, boolean $noWrap)
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the origin is not valid (prototypes).
     */
    public function setOrigin()
    {
        $args = func_get_args();

        if (isset($args[0]) && is_string($args[0])) {
            $this->origin = $args[0];
        } elseif (isset($args[0]) && ($args[0] instanceof Coordinate)) {
            $this->origin = $args[0];
        } elseif ((isset($args[0]) && is_numeric($args[0])) && (isset($args[1]) && is_numeric($args[1]))) {
            if ($this->origin === null) {
                $this->origin = new Coordinate();
            }

            $this->origin->setLatitude($args[0]);
            $this->origin->setLongitude($args[1]);

            if (isset($args[2]) && is_bool($args[2])) {
                $this->origin->setNoWrap($args[2]);
            }
        } else {
            throw DirectionsException::invalidDirectionsRequestOrigin();
        }
    }

    /**
     * Checks if the directions request has a departure time.
     *
     * @return boolean TRUE if the directions request has a departure time else FALSE.
     */
    public function hasDepartureTime()
    {
        return $this->departureTime !== null;
    }

    /**
     * Gets the directions request departure time.
     *
     * @return \DateTime The directions request departure time.
     */
    public function getDepartureTime()
    {
        return $this->departureTime;
    }

    /**
     * Sets the directions departure time
     *
     * @param \DateTime $departureTime The directions departure time.
     */
    public function setDepartureTime(DateTime $departureTime = null)
    {
        $this->departureTime = $departureTime;
    }

    /**
     * Checks if the directions request has an arrival time.
     *
     * @return boolean TRUE if the directions request has an arrival time else FALSE.
     */
    public function hasArrivalTime()
    {
        return $this->arrivalTime !== null;
    }

    /**
     * Gets the directions request arrival time.
     *
     * @return \DateTime The directions request arrival time.
     */
    public function getArrivalTime()
    {
        return $this->arrivalTime;
    }

    /**
     * Sets the directions arrival time
     *
     * @param \DateTime $arrivalTime The directions arrival time.
     */
    public function  setArrivalTime(DateTime $arrivalTime = null)
    {
        $this->arrivalTime = $arrivalTime;
    }

    /**
     * Checks if the directions request has a provide route alternatives flag.
     *
     * @return boolean TRUE if the directions request has a provide route alternative flag else FALSE.
     */
    public function hasProvideRouteAlternatives()
    {
        return $this->provideRouteAlternatives !== null;
    }

    /**
     * Checks if the directions request provides route alternatives.
     *
     * @return boolean TRUE if the directions request provides route alternatives else FALSE.
     */
    public function getProvideRouteAlternatives()
    {
        return $this->provideRouteAlternatives;
    }

    /**
     * Sets if the directions request provides route alternatives.
     *
     * @param boolean $provideRouteAlternatives TRUE if the directions request provides route alternatives else FALSE.
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the provide route alternatives flag is not valid.
     */
    public function setProvideRouteAlternatives($provideRouteAlternatives = null)
    {
        if (!is_bool($provideRouteAlternatives) && ($provideRouteAlternatives !== null)) {
            throw DirectionsException::invalidDirectionsRequestProvideRouteAlternatives();
        }

        $this->provideRouteAlternatives = $provideRouteAlternatives;
    }

    /**
     * Checks if the directions request has a region.
     *
     * @return boolean TRUE if the directions request has a region else FALSE.
     */
    public function hasRegion()
    {
        return $this->region !== null;
    }

    /**
     * Gets the directions request region.
     *
     * @return string The direction request region.
     */
    public function getRegion()
    {
        return $this->region;
    }

    /**
     * Sets the directions request region.
     *
     * @param string $region The directions request region.
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the region is not valid.
     */
    public function setRegion($region = null)
    {
        if ((!is_string($region) || (strlen($region) !== 2)) && ($region !== null)) {
            throw DirectionsException::invalidDirectionsRequestRegion();
        }

        $this->region = $region;
    }

    /**
     * Checks if the directions request has a language.
     *
     * @return boolean TRUE if the directions request has a language else FALSE.
     */
    public function hasLanguage()
    {
        return $this->language !== null;
    }

    /**
     * Gets the directions request language.
     *
     * @return string The direction request language.
     */
    public function getLanguage()
    {
        return $this->language;
    }

    /**
     * Sets the directions request language.
     *
     * @param string $language The directions request language.
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the language is not valid.
     */
    public function setLanguage($language = null)
    {
        if ((!is_string($language) || ((strlen($language) !== 2) && (strlen($language) !== 5))) && ($language !== null)) {
            throw DirectionsException::invalidDirectionsRequestLanguage();
        }

        $this->language = $language;
    }

    /**
     * Checks if the directions request has a travel mode.
     *
     * @return boolean TRUE if the directions request has a travel mode else FALSE.
     */
    public function hasTravelMode()
    {
        return $this->travelMode !== null;
    }

    /**
     * Gets the directions request travel mode.
     *
     * @return string The directions request travel mode.
     */
    public function getTravelMode()
    {
        return $this->travelMode;
    }

    /**
     * Sets the directions request travel mode.
     *
     * @param string $travelMode The directions request travel mode.
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the travel mode is not valid.
     */
    public function setTravelMode($travelMode = null)
    {
        if (!in_array($travelMode, TravelMode::getTravelModes()) && ($travelMode !== null)) {
            throw DirectionsException::invalidDirectionsRequestTravelMode();
        }

        $this->travelMode = $travelMode;
    }

    /**
     * Checks if the directions request has a unit system.
     *
     * @return boolean TRUE if the directions request has a unit system else FALSE.
     */
    public function hasUnitSystem()
    {
        return $this->unitSystem !== null;
    }

    /**
     * Gets the directions request unit system.
     *
     * @return string The directions request unit system.
     */
    public function getUnitSystem()
    {
        return $this->unitSystem;
    }

    /**
     * Sets  the directions request unit system.
     *
     * @param string $unitSystem The directions request unit system.
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the unit system is not valid.
     */
    public function setUnitSystem($unitSystem = null)
    {
        if (!in_array($unitSystem, UnitSystem::getUnitSystems()) && ($unitSystem !== null)) {
            throw DirectionsException::invalidDirectionsRequestUnitSystem();
        }

        $this->unitSystem = $unitSystem;
    }

    /**
     * Checks if the directions request has waypoints.
     *
     * @return boolean TRUE if the directions request has waypoints else FALSE.
     */
    public function hasWaypoints()
    {
        return !empty($this->waypoints);
    }

    /**
     * Gets the directions request waypoints.
     *
     * @return array The directions request waypoints.
     */
    public function getWaypoints()
    {
        return $this->waypoints;
    }

    /**
     * Sets the directions request waypoints.
     *
     * @param array $waypoints The directions request waypoints.
     */
    public function setWaypoints(array $waypoints = array())
    {
        $this->waypoints = array();

        foreach ($waypoints as $waypoint) {
            $this->addWaypoint($waypoint);
        }
    }

    /**
     * Adds a waypoint to the directions request.
     *
     * Available prototypes:
     * - function addWaypoint(Ivory\GoogleMap\Services\Directions\DirectionsWaypoint $waypoint)
     * - function addWaypoint(string $location)
     * - function addWaypoint(double $latitude, double $longitude, boolean $noWrap)
     * - function addWaypoint(Ivory\GoogleMap\Base\Coordinate $location)
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the waypoint is not valid (prototypes).
     */
    public function addWaypoint()
    {
        $args = func_get_args();

        if (isset($args[0]) && ($args[0] instanceof DirectionsWaypoint)) {
            $this->waypoints[] = $args[0];
        } elseif ((isset($args[0]) && is_numeric($args[0])) && (isset($args[1]) && is_numeric($args[1]))) {
            $waypoint = new DirectionsWaypoint();
            $waypoint->setLocation($args[0], $args[1]);

            if (isset($args[2]) && is_bool($args[2])) {
                $waypoint->getLocation()->setNoWrap($args[2]);
            }

            $this->waypoints[] = $waypoint;
        } elseif (isset($args[0]) && (is_string($args[0]) || ($args[0] instanceof Coordinate))) {
            $waypoint = new DirectionsWaypoint();
            $waypoint->setLocation($args[0]);

            $this->waypoints[] = $waypoint;
        } else {
            throw DirectionsException::invalidDirectionsRequestWaypoint();
        }
    }

    /**
     * Checks if the directions request has a sensor.
     *
     * @return boolean TRUE if the directions request has a sensor else FALSE.
     */
    public function hasSensor()
    {
        return $this->sensor;
    }

    /**
     * Sets the directions request sensor.
     *
     * @param boolean $sensor TRUE if the directions request has a sensor else FALSE.
     *
     * @throws \Ivory\GoogleMap\Exception\DirectionsException If the sensor flag is not valid.
     */
    public function setSensor($sensor)
    {
        if (!is_bool($sensor)) {
            throw DirectionsException::invalidDirectionsRequestSensor();
        }

        $this->sensor = $sensor;
    }

    /**
     * Checks if the directions request is valid.
     *
     * @return boolean TRUE if the directions request is valid else FALSE.
     */
    public function isValid()
    {
        $isValid = $this->hasDestination() && $this->hasOrigin();

        for ($i = 0; $isValid && ($i < count($this->waypoints)); $i++) {
            $isValid = $this->waypoints[$i]->isValid();
        }

        if ($this->getTravelMode() === TravelMode::TRANSIT) {
            $isValid = $this->hasArrivalTime() || $this->hasDepartureTime();
        }

        return $isValid;
    }
}


<?php
/**
 * GitHub.
 *
 * @author @jaswrks
 * @copyright WebSharks™
 */
declare(strict_types=1);
namespace WebSharks\Core\Classes\Core\Utils;

use WebSharks\Core\Classes;
use WebSharks\Core\Interfaces;
use WebSharks\Core\Traits;
#
use WebSharks\Core\Classes\Core\Error;
use WebSharks\Core\Classes\Core\Base\Exception;
#
use function assert as debug;
use function get_defined_vars as vars;

/**
 * GitHub.
 *
 * @since 170124.74961 GitHub utils.
 */
class GitHub extends Classes\Core\Base\Core
{
    /**
     * Cache directory.
     *
     * @since 170124.74961
     *
     * @type string
     */
    protected $cache_dir;

    /**
     * Class constructor.
     *
     * @since 170124.74961 GitHub utils.
     *
     * @param Classes\App $App Instance of App.
     */
    public function __construct(Classes\App $App)
    {
        parent::__construct($App);

        if (!$this->App->Config->©fs_paths['©cache_dir']) {
            throw $this->c::issue('Missing cache directory.');
        }
        $this->cache_dir = $this->App->Config->©fs_paths['©cache_dir'].'/github';
    }

    /**
     * GET raw response.
     *
     * @since 170124.74961 Initial release.
     *
     * @param string $url  API URL.
     * @param array  $args Additional args.
     *
     * @return \StdClass Including a boolean `success` property.
     */
    public function getRaw(string $url, array $args = []): \StdClass
    {
        $default_args = [
            'cache'            => true,
            'cache_max_age'    => strtotime('-15 minutes'),
            'api_access_token' => $this->App->Config->©github['©api_access_token'],
        ];
        $args += $default_args; // Merge defaults.

        $time = time(); // Current time.

        $args['cache']            = (bool) $args['cache'];
        $args['cache_max_age']    = max(0, min($time, (int) $args['cache_max_age']));
        $args['api_access_token'] = (string) $args['api_access_token'];

        $cache_sha1            = sha1($url.$args['api_access_token']);
        $cache_dir_permissions = $this->App->Config->©fs_permissions['©transient_dirs'];
        $cache_dir             = $this->cache_dir.'/'.$this->c::sha1ModShardId($cache_sha1, true);
        $cache_file            = $cache_dir.'/'.$cache_sha1; // Hash location.

        if ($args['cache'] && is_file($cache_file) && filemtime($cache_file) >= $args['cache_max_age']) {
            return $response_object = json_decode(file_get_contents($cache_file));
        }
        $response = $this->c::remoteRequest('GET::'.$url, [
            'headers' => [
                'accept: application/vnd.github.v3.raw',
                'authorization: token '.$args['api_access_token'],
            ],
            'return_array' => true,
        ]); // Expecting the API to return file contents.

        $response_object = (object) [
            'success'  => $response['code'] === 200,
            'data'     => &$response['body'],
            'response' => $response,
        ];
        if ($args['cache']) {
            if (!is_dir($cache_dir)) {
                mkdir($cache_dir, $cache_dir_permissions, true);
            }
            file_put_contents($cache_file, json_encode($response_object));
        }
        return $response_object;
    }

    /**
     * GET JSON response.
     *
     * @since 170124.74961 Initial release.
     *
     * @param string $url  API URL.
     * @param array  $args Additional args.
     *
     * @return \StdClass Including a boolean `success` property.
     */
    public function getJson(string $url, array $args = []): \StdClass
    {
        $default_args = [
            'cache'            => true,
            'cache_max_age'    => strtotime('-15 minutes'),
            'api_access_token' => $this->App->Config->©github['©api_access_token'],
        ];
        $args += $default_args; // Merge defaults.

        $time = time(); // Current time.

        $args['cache']            = (bool) $args['cache'];
        $args['cache_max_age']    = max(0, min($time, (int) $args['cache_max_age']));
        $args['api_access_token'] = (string) $args['api_access_token'];

        $cache_sha1            = sha1($url.$args['api_access_token']);
        $cache_dir_permissions = $this->App->Config->©fs_permissions['©transient_dirs'];
        $cache_dir             = $this->cache_dir.'/'.$this->c::sha1ModShardId($cache_sha1, true);
        $cache_file            = $cache_dir.'/'.$cache_sha1; // Hash location.

        if ($args['cache'] && is_file($cache_file) && filemtime($cache_file) >= $args['cache_max_age']) {
            return $response_object = json_decode(file_get_contents($cache_file));
        }
        $response = $this->c::remoteRequest('GET::'.$url, [
            'headers' => [
                'accept: application/json',
                'authorization: token '.$args['api_access_token'],
            ],
            'return_array' => true,
        ]); // Expecting the API to return JSON.

        $response_object = (object) [
            'success'  => $response['code'] && $response['code'] < 400,
            'data'     => json_decode($response['body']),
            'response' => $response,
        ];
        if ($args['cache']) {
            if (!is_dir($cache_dir)) {
                mkdir($cache_dir, $cache_dir_permissions, true);
            }
            file_put_contents($cache_file, json_encode($response_object));
        }
        return $response_object;
    }

    /**
     * Submit JSON data.
     *
     * @since 170124.74961 Initial release.
     *
     * @param string $url  API URL.
     * @param mixed  $data Data to submit.
     * @param array  $args Additional args.
     *
     * @return \StdClass Including a boolean `success` property.
     */
    public function submitJson(string $url, $data, array $args = []): \StdClass
    {
        if (!is_string($data)) {
            $data = json_encode($data);
        }
        $default_args = [
            'method'           => 'POST',
            'cache'            => false,
            'cache_max_age'    => strtotime('-15 minutes'),
            'api_access_token' => $this->App->Config->©github['©api_access_token'],
        ];
        $args += $default_args; // Merge defaults.

        $time = time(); // Current time.

        $args['method']           = (string) $args['method'];
        $args['cache']            = (bool) $args['cache'];
        $args['cache_max_age']    = max(0, min($time, (int) $args['cache_max_age']));
        $args['api_access_token'] = (string) $args['api_access_token'];

        $cache_sha1            = sha1($args['method'].$url.$args['api_access_token'].serialize($data));
        $cache_dir_permissions = $this->App->Config->©fs_permissions['©transient_dirs'];
        $cache_dir             = $this->cache_dir.'/'.$this->c::sha1ModShardId($cache_sha1, true);
        $cache_file            = $cache_dir.'/'.$cache_sha1; // Hash location.

        if ($args['cache'] && is_file($cache_file) && filemtime($cache_file) >= $args['cache_max_age']) {
            return $response_object = json_decode(file_get_contents($cache_file));
        }
        $response = $this->c::remoteRequest($args['method'].'::'.$url, [
            'headers' => [
                'accept: application/json',
                'content-type: application/json',
                'authorization: token '.$args['api_access_token'],
            ],
            'body'         => $data,
            'return_array' => true,
        ]); // Expecting the API to return JSON.

        $response_object = (object) [
            'success'  => $response['code'] && $response['code'] < 400,
            'data'     => json_decode($response['body']),
            'response' => $response,
        ];
        if ($args['cache']) {
            if (!is_dir($cache_dir)) {
                mkdir($cache_dir, $cache_dir_permissions, true);
            }
            file_put_contents($cache_file, json_encode($response_object));
        }
        return $response_object;
    }

    /**
     * POST JSON data.
     *
     * @since 170124.74961 Initial release.
     *
     * @param string $url  API URL.
     * @param mixed  $data Data to POST.
     * @param array  $args Additional args.
     *
     * @return \StdClass Including a boolean `success` property.
     */
    public function postJson(string $url, $data, array $args = []): \StdClass
    {
        return $this->submitJson($url, $data, array_merge($args, ['method' => 'POST']));
    }

    /**
     * PATCH JSON data.
     *
     * @since 170124.74961 Initial release.
     *
     * @param string $url  API URL.
     * @param mixed  $data Data to PATCH.
     * @param array  $args Additional args.
     *
     * @return \StdClass Including a boolean `success` property.
     */
    public function patchJson(string $url, $data, array $args = []): \StdClass
    {
        return $this->submitJson($url, $data, array_merge($args, ['method' => 'PATCH']));
    }

    /**
     * Issues refs.
     *
     * @since 170124.74961 GitHub utils.
     *
     * @param mixed $value   Input value.
     * @param array $args    Additional args.
     * @param array $___refs Internal use only.
     *
     * @return array All found issue refs.
     */
    public function issueRefs($value, array $args = [], array $___refs = []): array
    {
        if (is_array($value) || is_object($value)) {
            foreach ($value as $_key => $_value) {
                $___refs += $this->issueRefs($_value, $args, $___refs);
            } // unset($_key, $_value);
            return $___refs;
        }
        $markdown = (string) $value;

        if (!isset($markdown[0])) {
            return $markdown;
        }
        $default_args = [
            'current_owner' => '',
            'current_repo'  => '',
        ];
        $args += $default_args; // Merge defaults.

        $current_owner = (string) $args['current_owner'];
        $current_repo  = (string) $args['current_repo'];
        $have_currents = $current_owner && $current_repo;

        $current_owner_regex = '(?<owner>'.$this->c::escRegex($current_owner).')';
        $owner_regex         = '(?<owner>[a-z][a-z0-9_\-]*?[a-z0-9])';
        $repo_regex          = '(?<repo>[a-z][a-z0-9_\-]*?[a-z0-9])';
        $issue_regex         = '(?<issue>[0-9]+)';
        $comment_regex       = '(?<comment>#[^#\s]+)?';

        $pull_url_regex            = '/(?<=^|[\s;,(<])https?\:\/\/github\.com\/'.$owner_regex.'\/'.$repo_regex.'\/(?<is_pull>pull)\/'.$issue_regex.$comment_regex.'(?=$|[\s.!?;,)>])/ui';
        $issue_url_regex           = '/(?<=^|[\s;,(<])https?\:\/\/github\.com\/'.$owner_regex.'\/'.$repo_regex.'\/issues\/'.$issue_regex.$comment_regex.'(?=$|[\s.!?;,)>])/ui';
        $owner_repo_issue_regex    = '/(?<=^|[\s;,(])'.$owner_regex.'\/'.$repo_regex.'#'.$issue_regex.'(?=$|[\s.!?;,)])/ui';
        $current_owner_issue_regex = $have_currents ? '/(?<=^|[\s;,(])'.$current_owner_regex.'#'.$issue_regex.'(?=$|[\s.!?;,)])/ui' : '';
        $issue_number_regex        = $have_currents ? '/(?<=^|[\s;,(])#'.$issue_regex.'(?=$|[\s.!?;,)])/ui' : '';

        foreach ([$pull_url_regex, $issue_url_regex, $owner_repo_issue_regex, $current_owner_issue_regex, $issue_number_regex] as $_regex) {
            if (!$_regex) { // Skip if empty; e.g., currents not available.
                continue; // Not possible; regex is empty.
            }
            $markdown = preg_replace_callback($_regex, function ($m) use (&$___refs, $current_owner, $current_repo, $have_currents) {
                if (!$have_currents && (empty($m['owner']) || empty($m['repo']))) {
                    return ''; // Not possible.
                }
                $is_pull = isset($m['is_pull']);
                $owner = !empty($m['owner']) ? $m['owner'] : $current_owner;
                $repo = !empty($m['repo']) ? $m['repo'] : $current_repo;
                $number = $m['issue']; // Always have this.
                $comment = $m['comment'] ?? '';

                $___refs[($is_pull ? 'PR:' : '').$owner.'/'.$repo.'#'.$number.$comment] = [
                    'is_pull' => $is_pull,
                    'owner'   => $owner,
                    'repo'    => $repo,
                    'number'  => $number,
                    'comment' => $comment,
                ];
                return '';
            }, $markdown);
        } // unset($_regex); // Housekeeping.

        return $___refs;
    }

    /**
     * Markdown issue refs.
     *
     * @since 170124.74961 GitHub utils.
     *
     * @param mixed $value Input value.
     * @param array $args  Additional args.
     *
     * @return string|array|object Markdown'd issue refs.
     */
    public function mdIssueRefs($value, array $args = [])
    {
        if (is_array($value) || is_object($value)) {
            foreach ($value as $_key => &$_value) {
                $_value = $this->mdIssueRefs($_value, $args);
            } // unset($_key, $_value);
            return $value;
        }
        $markdown = (string) $value;

        if (!isset($markdown[0])) {
            return $markdown;
        }
        $default_args = [
            'current_owner' => '',
            'current_repo'  => '',
        ];
        $args += $default_args; // Merge defaults.

        $current_owner = (string) $args['current_owner'];
        $current_repo  = (string) $args['current_repo'];
        $have_currents = $current_owner && $current_repo;

        $current_owner_regex = '(?<owner>'.$this->c::escRegex($current_owner).')';
        $owner_regex         = '(?<owner>[a-z][a-z0-9_\-]*?[a-z0-9])';
        $repo_regex          = '(?<repo>[a-z][a-z0-9_\-]*?[a-z0-9])';
        $issue_regex         = '(?<issue>[0-9]+)';
        $comment_regex       = '(?<comment>#[^#\s]+)?';

        $pull_url_regex            = '/(?<=^|[\s;,<]|(?<!\])\()https?\:\/\/github\.com\/'.$owner_regex.'\/'.$repo_regex.'\/(?<is_pull>pull)\/'.$issue_regex.$comment_regex.'(?=$|[\s.!?;,)>])/ui';
        $issue_url_regex           = '/(?<=^|[\s;,<]|(?<!\])\()https?\:\/\/github\.com\/'.$owner_regex.'\/'.$repo_regex.'\/issues\/'.$issue_regex.$comment_regex.'(?=$|[\s.!?;,)>])/ui';
        $owner_repo_issue_regex    = '/(?<=^|[\s;,]|(?<!\])\()'.$owner_regex.'\/'.$repo_regex.'#'.$issue_regex.'(?=$|[\s.!?;,)])/ui';
        $current_owner_issue_regex = $have_currents ? '/(?<=^|[\s;,]|(?<!\])\()'.$current_owner_regex.'#'.$issue_regex.'(?=$|[\s.!?;,)])/ui' : '';
        $issue_number_regex        = $have_currents ? '/(?<=^|[\s;,]|(?<!\])\()#'.$issue_regex.'(?=$|[\s.!?;,)])/ui' : '';

        $Tokenizer = $this->c::tokenize($markdown, ['md-fences']);
        $markdown  = &$Tokenizer->getString(); // By reference.

        foreach ([$pull_url_regex, $issue_url_regex, $owner_repo_issue_regex, $current_owner_issue_regex, $issue_number_regex] as $_regex) {
            if (!$_regex) { // Skip if empty; e.g., currents not available.
                continue; // Not possible; regex is empty.
            }
            $markdown = preg_replace_callback($_regex, function ($m) use ($current_owner, $current_repo, $have_currents) {
                if (!$have_currents && (empty($m['owner']) || empty($m['repo']))) {
                    return $m[0]; // Not possible.
                }
                $is_pull = isset($m['is_pull']);
                $owner = !empty($m['owner']) ? $m['owner'] : $current_owner;
                $repo = !empty($m['repo']) ? $m['repo'] : $current_repo;
                $number = $m['issue']; // Always have this.
                $comment = $m['comment'] ?? '';

                $url = 'https://github.com/'.urlencode($owner).
                       '/'.urlencode($repo).'/'.($is_pull ? 'pull' : 'issues').
                       '/'.urlencode($number).$comment;

                if ($have_currents && $owner === $current_owner) {
                    if ($repo === $current_repo) {
                        return '['.$this->c::escHtmlChars('#'.$number.($comment ? ' (comment)' : '')).']('.$url.')';
                    } else {
                        return '['.$this->c::escHtmlChars($repo.'#'.$number.($comment ? ' (comment)' : '')).']('.$url.')';
                    }
                } else {
                    return '['.$this->c::escHtmlChars($owner.'/'.$repo.'#'.$number.($comment ? ' (comment)' : '')).']('.$url.')';
                }
            }, $markdown);
        } // unset($_regex); // Housekeeping.

        return $markdown = $Tokenizer->restoreGetString();
    }
}


<?php
/**
 * Debugging utilities.
 *
 * @author @jaswrks
 * @copyright WebSharks™
 */
declare(strict_types=1);
namespace WebSharks\Core\Classes\Core\Utils;

use WebSharks\Core\Classes;
use WebSharks\Core\Interfaces;
use WebSharks\Core\Traits;
#
use WebSharks\Core\Classes\Core\Error;
use WebSharks\Core\Classes\Core\Base\Exception;
#
use function assert as debug;
use function get_defined_vars as vars;

/**
 * Debugging utilities.
 *
 * @since 160522 Debugging utilities.
 */
class Debugging extends Classes\Core\Base\Core implements Interfaces\ByteConstants
{
    /**
     * Logs directory.
     *
     * @since 160522 Debugging utilities.
     *
     * @type string Logs directory.
     */
    protected $logs_dir;

    /**
     * Max log file size.
     *
     * @since 160522 Debugging utilities.
     *
     * @type int Max log file size.
     */
    protected $max_log_file_size;

    /**
     * Max log file age.
     *
     * @since 160522 Debugging utilities.
     *
     * @type string Max log file age.
     */
    protected $max_log_file_age;

    /**
     * First-process writes by file.
     *
     * @since 160522 Debugging utilities.
     *
     * @type array First-process writes by file.
     */
    protected $first_process_writes;

    /**
     * Class constructor.
     *
     * @since 160522 Debugging utilities.
     *
     * @param Classes\App $App Instance.
     */
    public function __construct(Classes\App $App)
    {
        parent::__construct($App);

        if (!$this->App->Config->©fs_paths['©logs_dir']) {
            throw new Exception('Missing logs directory.');
        }
        $this->logs_dir             = $this->App->Config->©fs_paths['©logs_dir'].'/debug';
        $this->max_log_file_size    = $this::BYTES_IN_MB * 2;
        $this->max_log_file_age     = strtotime('-7 days');
        $this->first_process_writes = []; // Initialize.
    }

    /**
     * Log issue & generate exception.
     *
     * @since 160522 Debugging utilities.
     *
     * @param mixed  $data  Data to log (e.g., {@link vars()}).
     * @param string $note  A brief note or description of the issue.
     * @param string $event Event name (e.g., {@link __METHOD__}).
     *
     * @return Exception An exception ready to be thrown.
     *
     * @internal If only one parameter is passed and it's a string, it's treated as the `$note`.
     */
    public function logIssue($data = [], string $note = '', string $event = ''): Exception
    {
        if (func_num_args() === 1 && is_string($data)) {
            $note = $data; // Use data as note.
            $data = []; // Data empty in this case.
        }
        $event = $event ?: $this->traceLogEventTrigger();

        if (mb_strpos($event, '#') === false) {
            $event .= '#issue';
        } // Assume this is an issue.

        $this->maybeLog($data, $note, $event);

        return new Exception($note ? $note : $event);
    }

    /**
     * Log for review.
     *
     * @since 160522 Debugging utilities.
     *
     * @param mixed  $data  Data to log (e.g., {@link vars()}).
     * @param string $note  A brief note or description of the issue.
     * @param string $event Event name (e.g., {@link __METHOD__}).
     *
     * @return int Total number of bytes written.
     *
     * @internal If only one parameter is passed and it's a string, it's treated as the `$note`.
     */
    public function logReview($data = [], string $note = '', string $event = ''): int
    {
        if (func_num_args() === 1 && is_string($data)) {
            $note = $data; // Use data as note.
            $data = []; // Data empty in this case.
        }
        $event = $event ?: $this->traceLogEventTrigger();

        if (mb_strpos($event, '#') === false) {
            $event .= '#review';
        } // Assume this is for review.

        return $this->maybeLog($data, $note, $event);
    }

    /**
     * Maybe log for debugging.
     *
     * @since 160522 Debugging utilities.
     *
     * @param mixed  $data  Data to log (e.g., {@link vars()}).
     * @param string $note  A brief note or description of the issue.
     * @param string $event Event name (e.g., {@link __METHOD__}).
     *
     * @return int Total number of bytes written.
     */
    protected function maybeLog($data = [], string $note = '', string $event = ''): int
    {
        if (!$this->App->Config->©debug['©log']) {
            return 0; // Not applicable.
        }
        $is_cli       = $this->c::isCli();
        $is_wordpress = $this->c::isWordPress();
        $event        = $this->cleanLogEvent($event);

        $lines[] = __('Time:').'         '.date('F jS, Y, g:i a T');
        $lines[] = __('Microtime:').'    '.number_format(microtime(true), 8, '.', '');
        $lines[] = __('Event:').'        '.($event ? $event : __('unknown event name'));
        $lines[] = __('Note:').'         '.($note ? $note : __('no note given by caller'))."\n";

        $lines[] = __('System:').'       '.PHP_OS.'; PHP v'.PHP_VERSION.' ('.PHP_SAPI.')';
        $lines[] = __('Software:').'     WSC v'.Classes\App::VERSION.// Core app class version.

                                         ($is_wordpress ? '; WP v'.$GLOBALS['wp_version'] : '').
                                         ($is_wordpress && defined('WC_VERSION') ? '; WC v'.WC_VERSION : '').

                                         ($this->App->Parent ? '; '.$this->App->Parent->Config->©brand['©acronym'].' v'.$this->App->Parent::VERSION : '').
                                        (!$this->App->is_ws_core ? '; '.$this->App->Config->©brand['©acronym'].' v'.$this->App::VERSION : '')."\n";

        if ($is_wordpress && ($user = wp_get_current_user()) && $user->exists()) {
            $lines[] = __('User:').'         #'.$user->ID.' @'.$user->user_login.' \''.$user->display_name.'\'';
        }
        $lines[] = __('User IP:').'      '.($is_cli ? __('n/a; CLI process') : $this->c::currentIp());
        $lines[] = __('User Agent:').'   '.($is_cli ? __('n/a; CLI process') : ($_SERVER['HTTP_USER_AGENT'] ?? ''));
        $lines[] = __('URL:').'          '.($is_cli ? __('n/a; CLI process') : $this->c::currentUrl())."\n";

        $lines[] = $this->c::mbTrim($this->c::dump($data, true), "\r\n"); // A dump of the data (variables).

        if (is_callable($this->App->Config->©debug['©log_callback'])) {
            $this->App->Config->©debug['©log_callback'](compact('event', 'data', 'note', 'lines'));
        }
        return $this->writeLogFileLines($event, $lines); // Writes the log entry.
    }

    /**
     * Write lines to log file.
     *
     * @since 160522 Debugging utilities.
     *
     * @param string $event Event name.
     * @param array  $lines Log entry lines.
     *
     * @return int Total number of bytes written.
     */
    protected function writeLogFileLines(string $event, array $lines): int
    {
        if (!$lines) { // No lines?
            return 0; // Stop; nothing to do here.
        }
        $this->prepareLogsDir(); // Prepares (and secures).

        if (mb_strpos($event, '#') !== false) {
            $file_name = mb_strstr($event, '#');
            $file_name = $this->c::nameToSlug($file_name);
            $file_name .= '.log'; // Add extension now.
        }
        $file_name    = $file_name ?: 'debug.log';
        $process_file = $this->logs_dir.'/process.'.$file_name;
        $file         = $this->logs_dir.'/'.$file_name;

        $this->maybeRotateLogFiles($file);

        $entry = implode("\n", $lines)."\n\n".str_repeat('-', 3)."\n\n";

        if (!isset($this->first_process_writes[$process_file])) {
            $this->first_process_writes[$process_file] = -1;
            @unlink($process_file); // First-process write.
        } // ↑ Empty file on the first write in each process.

        file_put_contents($process_file, $entry, LOCK_EX | FILE_APPEND);
        return (int) file_put_contents($file, $entry, LOCK_EX | FILE_APPEND);
    }

    /**
     * Cleans log event name.
     *
     * @since 160522 Debugging utilities.
     *
     * @param string $event Log event name.
     *
     * @return string Cleaned log event name.
     */
    protected function cleanLogEvent(string $event): string
    {
        if (($classes_pos = mb_strripos($event, '\\Classes\\')) !== false) {
            $event = mb_substr($event, $classes_pos + 9);
        } // This chops off `*\Classes\` from `__METHOD__`.
        $event = str_replace($this->App->namespace, '', $event);

        return $event = $this->c::mbTrim($event, '', '\\');
    }

    /**
     * Prepares the logs directory.
     *
     * @since 160522 Debugging utilities.
     */
    protected function prepareLogsDir()
    {
        if (is_dir($this->logs_dir)) {
            if (!is_writable($this->logs_dir)) {
                throw new Exception(sprintf('Logs directory not writable: `%1$s`.', $this->logs_dir));
            } // Always check to be sure the logs directory is still writable.
            return; // Otherwise; nothing to do here.
        }
        if (!mkdir($this->logs_dir, $this->App->Config->©fs_permissions['©transient_dirs'], true)) {
            throw new Exception(sprintf('Logs directory not writable: `%1$s`.', $this->logs_dir));
        } elseif (!$this->c::apacheHtaccessDeny($this->logs_dir)) {
            throw new Exception(sprintf('Unable to secure logs directory: `%1$s`.', $this->logs_dir));
        }
    }

    /**
     * Maybe rotate log files.
     *
     * @since 160522 Debugging utilities.
     *
     * @param string $file Absolute file path.
     */
    protected function maybeRotateLogFiles(string $file)
    {
        if (!$file || !is_file($file)) {
            return; // Nothing to do at this time.
        } elseif (filesize($file) < $this->max_log_file_size) {
            return; // Nothing to do at this time.
        } // Only rotate when log file becomes large.

        rename($file, $this->uniqueSuffixLogFile($file));

        foreach ($this->c::dirRegexRecursiveIterator($this->logs_dir, '/\.log$/ui') as $_Resource) {
            if ($_Resource->isFile() && $_Resource->getMTime() < $this->max_log_file_age) {
                unlink($_Resource->getPathname());
            }
        } // unset($_Resource); // Housekeeping.
    }

    /**
     * Trace log event trigger.
     *
     * @since 160522 Debugging utilities.
     *
     * @return string Log event trigger.
     */
    protected function traceLogEventTrigger(): string
    {
        // `[0]` is the call to this function.
        // `[1]` is the call to the utility function in this class.
        $callers = $this->c::backtraceCallers(2);
        return $callers ? $callers[0] : '';
    }

    /**
     * Unique suffix log file.
     *
     * @since 160522 Debugging utilities.
     *
     * @param string $file Absolute file path.
     *
     * @return string New unique (suffixed) log file.
     */
    protected function uniqueSuffixLogFile(string $file): string
    {
        return preg_replace('/\.log$/ui', '', $file).'-'.$this->c::uniqueId().'.log';
    }
}


<?php
/**
 * URL remote utilities.
 *
 * @author @jaswrks
 * @copyright WebSharks™
 */
declare(strict_types=1);
namespace WebSharks\Core\Classes\Core\Utils;

use WebSharks\Core\Classes;
use WebSharks\Core\Interfaces;
use WebSharks\Core\Traits;
#
use WebSharks\Core\Classes\Core\Error;
use WebSharks\Core\Classes\Core\Base\Exception;
#
use function assert as debug;
use function get_defined_vars as vars;

/**
 * URL remote utilities.
 *
 * @since 150424 Initial release.
 */
class UrlRemote extends Classes\Core\Base\Core
{
    /**
     * Regex.
     *
     * @since 170215.53419
     *
     * @type string
     */
    protected $method_regex;

    /**
     * Can follow?
     *
     * @since 170215.53419
     *
     * @type bool
     */
    protected $can_follow_location;

    /**
     * Class constructor.
     *
     * @since 170215.53419 Enhancing.
     *
     * @param Classes\App $App Instance of App.
     */
    public function __construct(Classes\App $App)
    {
        parent::__construct($App);

        $this->method_regex          = '/^(?<method>[^\/:]+)\:{2}(?<url>.+)/ui';
        $this->can_follow_location   = !filter_var(ini_get('safe_mode'), FILTER_VALIDATE_BOOLEAN) && !ini_get('open_basedir');
    }

    /**
     * Remote HTTP communication.
     *
     * @param string $url  A URL to connect to.
     * @param array  $args Connection arguments.
     *
     * @return string|\StdClass|array Based on arguments.
     */
    public function request(string $url, array $args = [])
    {
        return $this->curl($url, $args);
    }

    /**
     * Remote HTTP communication.
     *
     * @param string $url  A URL to connect to.
     * @param array  $args Connection arguments.
     *
     * @return \StdClass Always returns a full response.
     */
    public function response(string $url, array $args = []): \StdClass
    {
        return $this->curl($url, array_merge($args, ['return' => 'object']));
    }

    /**
     * cURL for remote HTTP communication.
     *
     * @since 170215.53419 Replaces `requestCurl()`.
     * @since 170215.53419 Deprecated `return_array`.
     *
     * @param string $url  A URL to connect to.
     * @param array  $args Connection arguments.
     *
     * @return string|\StdClass|array Based on arguments.
     */
    protected function curl(string $url, array $args = [])
    {
        # Parse arguments.

        $default_args = [
            'headers'         => [],
            'body'            => '',
            'cookie_file'     => '',
            'max_redirects'   => 5,
            'max_con_secs'    => 20,
            'max_stream_secs' => 20,
            'max_stream_size' => 0,
            'fail_on_error'   => true,
            'return_array'    => false, // Deprecated.
            'return'          => '', // Use this instead.
        ];
        $args = array_merge($default_args, $args);
        $args = array_intersect_key($args, $default_args);

        if (!empty($args['return_array']) && empty($args['return'])) {
            $args['return'] = 'array'; // Back compat.
        } // NOTE: Please use `return` instead of `return_array`.
        unset($args['return_array']); // Ditch this old argument now.

        # Extract and sanitize all args.

        extract($args); // Typify args.

        $headers         = (array) $headers;
        $cookie_file     = (string) $cookie_file;
        $max_redirects   = max(0, (int) $max_redirects);
        $max_con_secs    = max(1, (int) $max_con_secs);
        $max_stream_secs = max(1, (int) $max_stream_secs);
        $max_stream_size = max(0, (int) $max_stream_size);
        $fail_on_error   = (bool) $fail_on_error;
        $return          = (string) $return;

        # Parse request method from URL; e.g., `POST::`

        $method = ''; // Initialize, parsed below.

        if (mb_stripos($url, '::') !== false && preg_match($this->method_regex, $url, $_m)) {
            $method = mb_strtoupper($_m['method']);
            $url    = $_m['url']; // URL after `::`.

            if ($method === 'HEAD' && !$return) {
                $return = 'array'; // Default for `HEAD` requests.
            } // This preserves back compat, otherwise it would be `object`.
        } // unset($_m); // Housekeeping.

        # Validate URL.

        if (!$url) {
            $response = [
                'code'    => 0,
                'headers' => [],
                'body'    => '',
                'info'    => [],
                'errno'   => 0,
                'error'   => '',
            ];
            if ($return === 'object') {
                return (object) $response;
            } elseif ($return === 'array') {
                return $response;
            } else { // Body only.
                return $response['body'];
            }
        }
        # Convert body to a string.

        if (is_array($body) || is_object($body)) {
            $body = $this->c::buildUrlQuery((array) $body);
        } else {
            $body = (string) $body;
        }
        # Make sure we always have a `user-agent`.

        foreach ($headers as $_key => $_header) {
            if (mb_stripos($_header, 'user-agent:') === 0) {
                $_has_user_agent = true;
                break; // NOTE: This stops on the first `user-agent` header, even if it's empty.
                // If it's empty, or any thereafter are empty, we assume that is the desired behavior.
                // i.e., To force an empty `user-agent` header for whatever reason.
            }
        } // unset($_key, $_header); // Housekeeping.

        if (empty($_has_user_agent)) {
            $headers[]      = 'user-agent: '.__METHOD__;
        }
        # Setup header collection sub-routine.

        $curl_headers           = ''; // Initialize.
        $collect_output_headers = function ($curl, $headers) use (&$curl_headers) {
            $curl_headers .= $headers; // Write.
            return strlen($headers); // Bytes written.
        };
        # Determine if we can follow location headers.

        $follow_location = $max_redirects > 0 && $this->can_follow_location;
        $max_redirects   = !$follow_location ? 0 : $max_redirects;

        # Configure cURL options.

        $curl_opts = [
            CURLOPT_URL            => $url,

            CURLOPT_CONNECTTIMEOUT => $max_con_secs,
            CURLOPT_TIMEOUT        => $max_stream_secs,

            CURLOPT_ENCODING       => '',
            CURLOPT_HTTPHEADER     => $headers,

            CURLOPT_FAILONERROR    => $fail_on_error,
            CURLOPT_FOLLOWLOCATION => $follow_location,
            CURLOPT_MAXREDIRS      => $max_redirects,

            CURLOPT_HEADER         => false,
            CURLOPT_HEADERFUNCTION => $collect_output_headers,

            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_NOBODY         => $method === 'HEAD',
        ];
        if ($body && $method !== 'HEAD') {
            if ($method) {
                $curl_opts += [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_POSTFIELDS => $body];
            } else {
                $curl_opts += [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body];
            }
        } elseif ($method) { // No body data.
            $curl_opts += [CURLOPT_CUSTOMREQUEST => $method];
        }
        if ($cookie_file) { // Store cookies in this case.
            $curl_opts += [CURLOPT_COOKIEJAR => $cookie_file, CURLOPT_COOKIEFILE => $cookie_file];
        }
        if ($max_stream_size) { // Monitor bytes.
            $curl_opts += [CURLOPT_NOPROGRESS => false, CURLOPT_PROGRESSFUNCTION => #
                function ($r, int $s, int $d) use ($max_stream_size) {
                    return $d > $max_stream_size ? 1 : 0; // 1 = abort!
                }, ]; // See: <http://jas.xyz/2kw2FQe>
        }
        # Initialize cURL and perform request now.

        if (!($curl = curl_init())) {
            throw $this->c::issue(vars(), 'cURL init failure.');
        } // Highly unexpected, given there are no arguments here.

        curl_setopt_array($curl, $curl_opts);
        $curl_body = (string) curl_exec($curl);

        # Collect cURL info.

        $curl_info = curl_getinfo($curl);
        $curl_info = is_array($curl_info) ? $curl_info : [];
        $curl_code = (int) ($curl_info['http_code'] ?? 0);

        $curl_errno = (int) curl_errno($curl);
        $curl_error = (string) curl_error($curl);

        # Parse the headers that we collected, if any.

        $curl_headers = explode("\r\n\r\n", $this->c::mbTrim($curl_headers));
        $curl_headers = $curl_headers[count($curl_headers) - 1];
        // ↑ Last set of headers; in case of location redirects.

        $_curl_headers = $curl_headers;
        $curl_headers  = []; // Initialize.

        foreach (preg_split('/['."\r\n".']+/u', $_curl_headers, -1, PREG_SPLIT_NO_EMPTY) as $_line) {
            if (isset($_line[0]) && mb_strpos($_line, ':', 1) !== false) {
                list($_header, $_value)                                  = explode(':', $_line, 2);
                $curl_headers[mb_strtolower($this->c::mbTrim($_header))] = $this->c::mbTrim($_value);
            }
        } // unset($_curl_headers, $_line, $_header, $_value); // Housekeeping.

        # Check if failing on error. If so, empty the response body.

        if ($fail_on_error && ($curl_code === 0 || $curl_code >= 400 || $curl_errno)) {
            $curl_body = ''; // Empty body on error.
        }
        # Close cURL resource handle.

        curl_close($curl);

        # Return final response.

        $response = [
            'code'    => $curl_code,
            'headers' => $curl_headers,
            'body'    => $curl_body,
            'info'    => $curl_info,
            'errno'   => $curl_errno,
            'error'   => $curl_error,
        ];
        if ($this->App->Config->©debug['©log']) {
            $this->c::review([
                'url'      => $url,
                'args'     => $args,
                'response' => $response,
            ], 'Remote URL connection details.', __METHOD__.'#http');
        }
        if ($return === 'object') {
            return (object) $response;
        } elseif ($return === 'array') {
            return $response;
        } else { // Body only.
            return $response['body'];
        }
    }
}


<?php

namespace Kurenai;

use Kurenai\Contracts\Document;
use Symfony\Component\Yaml\Yaml;
use Kurenai\Exceptions\TooFewSectionsException;
use Symfony\Component\Yaml\Exception\ParseException;

class DocumentParser
{
    /**
     * Pattern to split the fields from the article body.
     */
    const SECTION_SPLITTER = '/\s+-{3,}\s+/';

    /**
     * Document object resolver.
     *
     * @var \Kurenai\Contracts\Document
     */
    protected $documentResolver;

    /**
     * Instantiate an instance optionally passing in a Documemt object resolver.
     *
     * @param  \Kurenai\Contracts\Document  $documentResolver
     */
    public function __construct(Contracts\Document $documentResolver)
    {
        $this->documentResolver = $documentResolver;
    }

    /**
     * Parse a markdown document with metadata.
     *
     * @param  string  $source
     *
     * @return \Kurenai\Contracts\Document
     */
    public function parse(string $source): Contracts\Document
    {
        $content = $this->parseContent($source);
        $metadata = $this->parseMetadata($this->parseHeader($source));

        return $this->buildDocument($content, $metadata);
    }

    /**
     * Build a Document from the parsed data.
     *
     * @param  string  $content
     * @param  array   $metadata
     *
     * @return \Kurenai\Contracts\Document
     */
    public function buildDocument(string $content, array $metadata): Contracts\Document
    {
        $document = $this->documentResolver;
        $document->setContent($content);
        $document->set($metadata);

        return $document;
    }

    /**
     * Parse the header section of the document.
     *
     * @param  string $source
     *
     * @return string
     */
    public function parseHeader(string $source): string
    {
        return $this->parseSection($source, 0);
    }

    /**
     * Parse the content section of the document.
     *
     * @param  string $source
     *
     * @return string
     */
    public function parseContent(string $source): string
    {
        return $this->parseSection($source, 1);
    }

    /**
     * Parse a dection of the document.
     *
     * @param  string  $source
     * @param  int  $offset
     *
     * @throws \Kurenai\Exceptions\TooFewSectionsException
     *
     * @return string
     */
    public function parseSection(string $source, int $offset): string
    {
        $sections = preg_split(self::SECTION_SPLITTER, $source, 2);

        if (count($sections) != 2) {
            throw new TooFewSectionsException();
        }

        return trim($sections[$offset]);
    }

    /**
     * Parse metadata into an array.
     *
     * @param  string  $source
     *
     * @return array
     */
    public function parseMetadata(string $source): array
    {
        $yaml = Yaml::parse($source);

        if (! is_array($yaml)) {
            throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, null);
        }

        return $yaml;
    }
}