<?php

class GooglePlacesClient
{
    public function get($url)
    {
        $curl = curl_init();
        $options = array(
            CURLOPT_URL            => $url,
            CURLOPT_HEADER         => false,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_RETURNTRANSFER => true,
        );
        curl_setopt_array($curl, $options);
        $response = curl_exec($curl);
        if ($error = curl_error($curl))
        {
            throw new \Exception('CURL Error: ' . $error);
        }
        curl_close($curl);
        return $response;
    }
}

class GooglePlaces
{
    public  $client    = '';
    public  $sleep     = 3;
    private $key       = '';
    private $base_url  = 'https://maps.googleapis.com/maps/api/place';
    private $method    = null;
    private $response  = null;

    public  $keyword   = null;
    public  $language  = 'en';
    public  $location  = null;
    public  $output    = 'json';
    public  $name      = null;
    public  $pagetoken = null;
    public  $radius    = null;
    public  $rankby    = 'prominence';
    public  $sensor    = false;
    public  $types     = null;
    public  $placeid   = null;
    public  $reference = null;
    public  $opennow   = null;
    public  $input     = null;

    public  $subradius = null;
    public  $getmax    = true;
    private $grid      = null;

    private $exceptions = array(
        'base_url', 'client', 'exceptions', 'getmax', 'grid', 'method',
        'output', 'pagetoken', 'response', 'sleep', 'subradius',
    );

    public function __construct($key, $client = false)
    {
        $this->key    = $key;
        $this->client = $client ? $client : new GooglePlacesClient();
    }

    function __set($variable, $value)
    {
        // Compensates for mixed variable naming
        $variable        = str_replace('_', '', strtolower($variable));
        $this->$variable = $value;
    }

    public function __call($method, $arguments)
    {
        $this->output = strtolower($this->output);

        if (!in_array($this->output, array('json', 'xml')))
        {
            throw new \Exception('Invalid output, please specify either "json" or "xml".');
        }

        $method     = $this->method = strtolower($method);
        $url        = implode('/', array($this->base_url, $method, $this->output));
        $parameters = array();
        $parameters = $this->parameterBuilder($parameters);
        $parameters = $this->methodChecker($parameters, $method);

        if (!empty($this->subradius))
        {
            return $this->subdivide($url, $parameters);
        }

        if ($this->pagetoken !== null)
        {
            $parameters['pagetoken'] = $this->pagetoken;
        }

        $result = $this->queryGoogle($url, $parameters);
        return $result;
    }

    /**
     * Loops through all of our variables to make a parameter list
     */
    private function parameterBuilder($parameters)
    {
        foreach (get_object_vars($this) as $variable => $value)
        {
            // Except these variables
            if (!in_array($variable, $this->exceptions))
            {
                // Assuming it's not null
                if ($value !== null)
                {
                    // Converts boolean to string
                    if (is_bool($value))
                    {
                        $value = $value ? 'true' : 'false';
                    }

                    switch ($variable)
                    {
                        // Allows LatLng to be passed as an array
                        case 'location':
                            if (is_array($value))
                            {
                                // Just in case it's an associative array
                                $value = array_values($value);
                                $value = $value[0] . ',' . $value[1];
                            }
                            break;

                        // Checks that it's a value rank by value
                        case 'rankby':
                            $value = strtolower($value);

                            if (!in_array($value, array('prominence', 'distance')))
                            {
                                throw new \Exception('Invalid rank by value, please specify either "prominence" or "distance".');
                            }
                            break;

                        // Allows types to be passed as an array
                        case 'types':
                            if (is_array($value))
                            {
                                $value = implode('|', $value);
                            }
                            break;
                    }

                    $parameters[$variable] = $value;
                }
            }
        }
        return $parameters;
    }

    /**
     * takes the parameters and method to throw exceptions or modify parameters as needed
     * @todo Method to sanity check passed types
     */
    private function methodChecker($parameters, $method)
    {
        if (!isset($parameters['pagetoken']))
        {
            switch ($method)
            {
                case 'nearbysearch':
                    if (!isset($parameters['location']))
                    {
                        throw new \Exception('You must specify a location before calling nearbysearch().');
                    }
                    elseif (isset($parameters['rankby']))
                    {
                        switch ($parameters['rankby'])
                        {
                            case 'distance':
                                if (!isset($parameters['keyword'])
                                    && !isset($parameters['name'])
                                    && !isset($parameters['types']))
                                {
                                    throw new \Exception('You much specify at least one of the following: "keyword", "name", "types".');
                                }

                                if (isset($parameters['radius']))
                                {
                                    unset($this->radius, $parameters['radius']);
                                }
                                break;

                            case 'prominence':
                                if (!isset($parameters['radius']))
                                {
                                    throw new \Exception('You must specify a radius.');
                                }
                                break;
                        }
                    }

                    break;

                case 'radarsearch':
                    if (!isset($parameters['location']))
                    {
                        throw new \Exception('You must specify a location before calling radarsearch().');
                    }
                    elseif (!isset($parameters['radius']))
                    {
                        throw new \Exception('You must specify a radius.');
                    }
                    elseif (empty($parameters['keyword'])
                        && empty($parameters['name'])
                        && empty($parameters['types']))
                    {
                        throw new \Exception('You much specify at least one of the following: "keyword", "name", "types".');
                    }

                    if (isset($parameters['rankby']))
                    {
                        unset($this->rankby, $parameters['rankby']);
                    }

                    break;

                case 'details':
                    if (!(isset($parameters['reference'])
                        ^ isset($parameters['placeid'])))
                    {
                        throw new \Exception('You must specify either a "placeid" or a "reference" (but not both) before calling details().');
                    }

                    if (isset($parameters['rankby']))
                    {
                        unset($this->rankby, $parameters['rankby']);
                    }

                    break;

                case 'autocomplete':
                    /*if (!isset($parameters['location']))
                    {
                        throw new \Exception('You must specify a location before calling autocomplete().');
                    }
                    elseif (!isset($parameters['radius']))
                    {
                        throw new \Exception('You must specify a radius.');
                    }
                    else*/if (empty($parameters['input']))
                    {
                        throw new \Exception('You much specify the user entered input string.');
                    }

                    break;
            }
        }

        return $parameters;
    }


    /**
     * Submits request via curl, sets the response, then returns the response
     */
    private function queryGoogle($url, $parameters)
    {
        if ($this->pagetoken !== null)
        {
            $parameters['pagetoken'] = $this->pagetoken;
            sleep($this->sleep);
        }

        // Couldn't seem to get http_build_query() to work right so...
        $querystring = '';

        foreach ($parameters as $variable => $value)
        {
            if ($querystring != '')
            {
                $querystring .= '&';
            }

            $querystring .= $variable . '=' . urlencode($value);
        }

        $response = $this->client->get($url . '?' . $querystring);

        if ($this->output == 'json')
        {
            $response = json_decode($response, true);

            if ($response === null)
            {
                throw new \Exception('The returned JSON was malformed or nonexistent.');
            }
        }
        else
        {
            throw new \Exception('XML is terrible, don\'t use it, ever.');
        }

        $this->response = $response;

        return $this->response;
    }

    /**
     * Returns the longitude equal to a given distance (meters) at a given latitude
     */
    public function meters2lng($meters, $latitude)
    {
        return $meters / (cos(deg2rad($latitude)) * 40075160 / 360);
    }

    /**
     * Returns the latitude equal to a given distance (meters)
     */
    public function meters2lat($meters)
    {
        return $meters / (40075160 / 360);
    }

    /**
     * Returns the aggregated responses for a subdivided search
     */
    private function subdivide($url, $parameters)
    {
        if ($this->subradius < 200)
        {
            throw new \Exception('Subradius should be at least 200 meters.');
        }

        $quotient = $parameters['radius'] / $this->subradius;

        if ($parameters['radius'] % $this->subradius || $quotient % 2)
        {
            throw new \Exception('Subradius should divide evenly into radius.');
        }

        $center    = explode(',', $parameters['location']);
        $centerlat = $center[0];
        $centerlng = $center[1];
        $count     = $quotient;
        $lati      = $this->meters2lat($this->subradius * 2);

        $this->grid['results'] = array();

        for ($i = $count / 2 * -1; $i <= $count / 2; $i++)
        {
            $lat  = $centerlat + $i * $lati;
            $lngi = $this->meters2lng($this->subradius * 2, $lat);

            for ($j = $count / 2 * -1; $j <= $count / 2; $j++)
            {
                $lng = $centerlng + $j * $lngi;
                $loc = $lat . ',' . $lng;

                $parameters['location'] = $loc;
                $parameters['radius']   = $this->subradius;

                $pagetoken = true;

                while ($pagetoken)
                {
                    $this->queryGoogle($url, $parameters);

                    $this->grid[$i][$j] = $this->response;

                    $this->grid['results'] = array_merge(
                        $this->grid['results'],
                        $this->response['results']
                    );

                    if (isset($this->response['next_page_token']))
                    {
                        $this->pagetoken = $this->response['next_page_token'];
                    }
                    else
                    {
                        $this->pagetoken = null;
                        $pagetoken       = false;
                    }
                }
            }
        }

        return $this->grid;
    }
}

