<?php namespace Arcanedev\LaravelHtml;

use Arcanedev\Html\Elements\{
    Button, File, Form, Input, Label, Select, Textarea
};
use Arcanedev\LaravelHtml\Bases\Builder;
use Arcanedev\LaravelHtml\Contracts\FormBuilder as FormBuilderContract;
use DateTime;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\Session\Session;
use Illuminate\Support\{
    Arr, Collection, Str
};

/**
 * Class     FormBuilder
 *
 * @package  Arcanedev\LaravelHtml
 * @author   ARCANEDEV <arcanedev.maroc@gmail.com>
 */
class FormBuilder extends Builder implements FormBuilderContract
{
    /* -----------------------------------------------------------------
     |  Properties
     | -----------------------------------------------------------------
     */

    /**
    * The HTML builder instance.
    *
    * @var \Arcanedev\LaravelHtml\Contracts\HtmlBuilder
    */
    protected $html;

    /**
    * The URL generator instance.
    *
    * @var \Illuminate\Contracts\Routing\UrlGenerator
    */
    protected $url;

    /**
    * The CSRF token used by the form builder.
    *
    * @var string
    */
    protected $csrfToken;

    /**
    * The session store implementation.
    *
    * @var \Illuminate\Contracts\Session\Session|\Illuminate\Session\Store
    */
    protected $session;

    /**
    * The current model instance for the form.
    *
    * @var \Illuminate\Database\Eloquent\Model|null
    */
    protected $model;

    /**
    * An array of label names we've created.
    *
    * @var array
    */
    protected $labels = [];

    /**
    * The reserved form open attributes.
    *
    * @var array
    */
    protected $reserved = ['method', 'url', 'route', 'action', 'files'];

    /**
    * The form methods that should be spoofed, in uppercase.
    *
    * @var array
    */
    protected $spoofedMethods = ['DELETE', 'PATCH', 'PUT'];

    /**
    * The types of inputs to not fill values on by default.
    *
    * @var array
    */
    protected $skipValueTypes = ['file', 'password', 'checkbox', 'radio'];

    /* -----------------------------------------------------------------
     |  Constructor
     | -----------------------------------------------------------------
     */

    /**
    * Create a new form builder instance.
    *
    * @param  \Arcanedev\LaravelHtml\Contracts\HtmlBuilder  $html
    * @param  \Illuminate\Contracts\Routing\UrlGenerator    $url
    * @param  \Illuminate\Contracts\Session\Session         $session
    */
    public function __construct(Contracts\HtmlBuilder $html, UrlGenerator $url, Session $session)
    {
        $this->url       = $url;
        $this->html      = $html;
        $this->csrfToken = $session->token();

        $this->setSessionStore($session);
    }

    /* -----------------------------------------------------------------
     |  Getters & Setters
     | -----------------------------------------------------------------
     */

    /**
     * Get the session store implementation.
     *
     * @return  \Illuminate\Contracts\Session\Session
     */
    public function getSessionStore()
    {
        return $this->session;
    }

    /**
     * Set the session store implementation.
     *
     * @param  \Illuminate\Contracts\Session\Session  $session
     *
     * @return self
     */
    public function setSessionStore(Session $session)
    {
        $this->session = $session;

        return $this;
    }

    /**
     * Set the model instance on the form builder.
     *
     * @param  \Illuminate\Database\Eloquent\Model|null  $model
     *
     * @return self
     */
    public function setModel($model)
    {
        $this->model = $model;

        return $this;
    }

    /**
     * Get the model instance on the form builder.
     *
     * @return \Illuminate\Database\Eloquent\Model
     */
    public function getModel()
    {
        return $this->model;
    }

    /**
     * Get the ID attribute for a field name.
     *
     * @param  string  $name
     * @param  array   $attributes
     *
     * @return string
     */
    public function getIdAttribute($name, array $attributes)
    {
        if (array_key_exists('id', $attributes))
            return $attributes['id'];

        if (in_array($name, $this->labels))
            return $name;

        return null;
    }

    /**
     * Get the value that should be assigned to the field.
     *
     * @param  string  $name
     * @param  mixed   $value
     *
     * @return mixed
     */
    public function getValueAttribute($name, $value = null)
    {
        if (is_null($name))
            return $value;

        if ( ! is_null($this->old($name)) && $name !== '_method')
            return $this->old($name);

        if ( ! is_null($value))
            return $value;

        return $this->getModelValueAttribute($name);
    }

    /**
     * Get the model value that should be assigned to the field.
     *
     * @param  string                               $name
     * @param  \Illuminate\Database\Eloquent\Model  $model
     *
     * @return mixed
     */
    private function getModelValueAttribute($name, $model = null)
    {
        $model = $model ?: $this->getModel();

        $key = self::transformKey($name);

        if (strpos($key, '.') !== false) {
            $keys = explode('.', $key, 2);

            return $this->getModelValueAttribute(
                $keys[1],
                $this->getModelValueAttribute($keys[0], $model)
            );
        }

        return method_exists($model, 'getFormValue')
            ? $model->getFormValue($key)
            : data_get($model, $key);
    }

    /**
     * Get a value from the session's old input.
     *
     * @param  string  $name
     *
     * @return mixed
     */
    public function old($name)
    {
        return ! is_null($this->session)
            ? $this->session->getOldInput(self::transformKey($name))
            : null;
    }

    /**
     * Transform key from array to dot syntax.
     *
     * @param  string  $key
     *
     * @return string
     */
    private static function transformKey($key)
    {
        return str_replace(
            ['.', '[]', '[', ']'],
            ['_', '', '.', ''],
            $key
        );
    }

    /**
     * Determine if the old input is empty.
     *
     * @return bool
     */
    public function oldInputIsEmpty()
    {
        return ! is_null($this->session)
            && (count($this->session->getOldInput()) === 0);
    }

    /* -----------------------------------------------------------------
     |  Main Methods
     | -----------------------------------------------------------------
     */

    /**
     * Open up a new HTML form.
     *
     * @param  array  $attributes
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function open(array $attributes = [])
    {
        $method = Str::upper(Arr::pull($attributes, 'method', 'POST'));

        return Form::make()
            ->method($method !== 'GET' ? 'POST' : $method)
            ->action($this->getAction($attributes))
            ->attributes(array_merge(
                ['accept-charset' => 'UTF-8'],
                Arr::except($attributes, $this->reserved)
            ))
            ->if(Arr::pull($attributes, 'files', false), function (Form $form) {
                return $form->acceptsFiles();
            })
            ->if(in_array($method, $this->spoofedMethods), function (Form $form) use ($method) {
                return $form->addChild($this->hidden('_method', $method));
            })
            ->if($method !== 'GET', function (Form $form) {
                return $form->addChild($this->token());
            })
            ->open();
    }

    /**
     * Create a new model based form builder.
     *
     * @param  mixed  $model
     * @param  array  $attributes
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function model($model, array $attributes = [])
    {
        return $this->setModel($model)->open($attributes);
    }

    /**
     * Close the current form.
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function close()
    {
        $this->labels = [];
        $this->setModel(null);

        return Form::make()->close();
    }

    /**
     * Generate a hidden field with the current CSRF token.
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function token()
    {
        return $this->hidden(
            '_token',
            empty($this->csrfToken) ? $this->session->token() : $this->csrfToken
        );
    }

    /**
     * Create a form label element.
     *
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     * @param  bool          $escaped
     *
     * @return \Arcanedev\Html\Elements\Label
     */
    public function label($name, $value = null, array $attributes = [], $escaped = true)
    {
        $this->labels[] = $name;

        $value = $value ?: Str::title(str_replace(['_', '-'], ' ', $name));

        return Label::make()
            ->for($name)
            ->attributes($attributes)
            ->html($escaped ? e($value) : $value);
    }

    /**
     * Create a form input field.
     *
     * @param  string        $type
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function input($type, $name, $value = null, array $attributes = [])
    {
        if ( ! in_array($type, $this->skipValueTypes))
            $value = $this->getValueAttribute($name, $value);

        return Input::make()
            ->type($type)
            ->attributeUnless(is_null($name), 'name', $name)
            ->attributeUnless(is_null($id = $this->getIdAttribute($name, $attributes)), 'id', $id)
            ->attributeUnless(is_null($value) || empty($value), 'value', $value)
            ->attributes($attributes);
    }

    /**
     * Create a text input field.
     *
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function text($name, $value = null, array $attributes = [])
    {
        return $this->input('text', $name, $value, $attributes);
    }

    /**
     * Create a password input field.
     *
     * @param  string  $name
     * @param  array   $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function password($name, array $attributes = [])
    {
        return $this->input('password', $name, null, $attributes);
    }

    /**
     * Create a hidden input field.
     *
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function hidden($name, $value = null, array $attributes = [])
    {
        return $this->input('hidden', $name, $value, $attributes);
    }

    /**
     * Create an e-mail input field.
     *
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function email($name, $value = null, array $attributes = [])
    {
        return $this->input('email', $name, $value, $attributes);
    }

    /**
     * Create a tel input field.
     *
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function tel($name, $value = null, array $attributes = [])
    {
        return $this->input('tel', $name, $value, $attributes);
    }

    /**
     * Create a number input field.
     *
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function number($name, $value = null, array $attributes = [])
    {
        return $this->input('number', $name, $value, $attributes);
    }

    /**
     * Create a date input field.
     *
     * @param  string  $name
     * @param  string  $value
     * @param  array   $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function date($name, $value = null, array $attributes = [])
    {
        if ($value instanceof DateTime)
            $value = $value->format('Y-m-d');

        return $this->input('date', $name, $value, $attributes);
    }

    /**
     * Create a datetime input field.
     *
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function datetime($name, $value = null, array $attributes = [])
    {
        if ($value instanceof DateTime)
            $value = $value->format(DateTime::RFC3339);

        return $this->input('datetime', $name, $value, $attributes);
    }

    /**
     * Create a datetime-local input field.
     *
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function datetimeLocal($name, $value = null, array $attributes = [])
    {
        if ($value instanceof DateTime)
            $value = $value->format('Y-m-d\TH:i');

        return $this->input('datetime-local', $name, $value, $attributes);
    }

    /**
     * Create a time input field.
     *
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function time($name, $value = null, array $attributes = [])
    {
        return $this->input('time', $name, $value, $attributes);
    }

    /**
     * Create a url input field.
     *
     * @param  string  $name
     * @param  string  $value
     * @param  array   $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function url($name, $value = null, array $attributes = [])
    {
        return $this->input('url', $name, $value, $attributes);
    }

    /**
     * Create a file input field.
     *
     * @param  string  $name
     * @param  array   $attributes
     *
     * @return \Arcanedev\Html\Elements\File
     */
    public function file($name, array $attributes = [])
    {
        return File::make()->name($name)->attributes($attributes);
    }

    /**
     * Create a textarea input field.
     *
     * @param  string  $name
     * @param  string  $value
     * @param  array   $attributes
     *
     * @return \Arcanedev\Html\Elements\Textarea
     */
    public function textarea($name, $value = null, array $attributes = [])
    {
        $id    = $this->getIdAttribute($name, $attributes);
        $size  = Arr::pull($attributes, 'size');
        $value = (string) $this->getValueAttribute($name, $value);

        return Textarea::make()
            ->name($name)
            ->attributeUnless(is_null($id), 'id', $id)
            ->unless(is_null($size), function (Textarea $elt) use ($size) {
                return $elt->size($size);
            })
            ->attributes($attributes)
            ->html($this->html->escape($value));
    }

    /**
     * Create a select box field.
     *
     * @param  string                                $name
     * @param  array|\Illuminate\Support\Collection  $list
     * @param  string|bool                           $selected
     * @param  array                                 $attributes
     * @param  array                                 $optionsAttributes
     * @param  array                                 $optgroupsAttributes
     *
     * @return \Arcanedev\Html\Elements\Select
     */
    public function select(
        $name,
        $list = [],
        $selected = null,
        array $attributes = [],
        array $optionsAttributes = [],
        array $optgroupsAttributes = []
    ) {
        return Select::make()
            ->name($name)
            ->options($list, $optionsAttributes, $optgroupsAttributes)
            ->attributes($attributes)
            ->attributeUnless(is_null($id = $this->getIdAttribute($name, $attributes)), 'id', $id)
            ->value($this->getValueAttribute($name, $selected));
    }

    /**
     * Create a select range field.
     *
     * @param  string  $name
     * @param  string  $begin
     * @param  string  $end
     * @param  string  $selected
     * @param  array   $attributes
     *
     * @return \Arcanedev\Html\Elements\Select
     */
    public function selectRange($name, $begin, $end, $selected = null, array $attributes = [])
    {
        $range = array_combine($range = range($begin, $end), $range);

        return $this->select($name, $range, $selected, $attributes);
    }

    /**
     * Create a select year field.
     *
     * @param  string  $name
     * @param  string  $begin
     * @param  string  $end
     * @param  string  $selected
     * @param  array   $attributes
     *
     * @return \Arcanedev\Html\Elements\Select
     */
    public function selectYear($name, $begin, $end, $selected = null, array $attributes = [])
    {
        return $this->selectRange($name, $begin, $end, $selected, $attributes);
    }

    /**
     * Create a select month field.
     *
     * @param  string  $name
     * @param  string  $selected
     * @param  array   $attributes
     * @param  string  $format
     *
     * @return \Arcanedev\Html\Elements\Select
     */
    public function selectMonth($name, $selected = null, array $attributes = [], $format = '%B')
    {
        $months = [];

        foreach(range(1, 12) as $month) {
            $months[$month] = strftime($format, mktime(0, 0, 0, $month, 1));
        }

        return $this->select($name, $months, $selected, $attributes);
    }

    /**
     * Create a checkbox input field.
     *
     * @param  string     $name
     * @param  mixed      $value
     * @param  bool|null  $checked
     * @param  array      $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function checkbox($name, $value = 1, $checked = null, array $attributes = [])
    {
        return $this->checkable('checkbox', $name, $value, $checked, $attributes);
    }

    /**
     * Create a radio button input field.
     *
     * @param  string  $name
     * @param  mixed   $value
     * @param  bool    $checked
     * @param  array   $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function radio($name, $value = null, $checked = null, array $attributes = [])
    {
        return $this->checkable('radio', $name, $value ?: $name, $checked, $attributes);
    }

    /**
     * Create a HTML reset input element.
     *
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Button
     */
    public function reset($value, array $attributes = [])
    {
        return $this->button($value, array_merge(['type' => 'reset'], $attributes));
    }

    /**
    * Create a HTML image input element.
    *
    * @param  string       $url
    * @param  string|null  $name
    * @param  array        $attributes
    *
     * @return \Arcanedev\Html\Elements\Input
    */
    public function image($url, $name = null, array $attributes = [])
    {
        return $this->input('image', $name, null, array_merge($attributes, [
            'src' => $this->url->asset($url),
        ]));
    }

    /**
     * Create a submit button element.
     *
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Button
     */
    public function submit($value = null, array $attributes = [])
    {
        return $this->button($value, array_merge(['type' => 'submit'], $attributes));
    }

    /**
     * Create a button element.
     *
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Button
     */
    public function button($value = null, array $attributes = [])
    {
        return Button::make()
            ->type(Arr::pull($attributes, 'type', 'button'))
            ->attributes($attributes)
            ->html($value);
    }

    /**
     * Create a color input field.
     *
     * @param  string        $name
     * @param  string|mixed  $value
     * @param  array         $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    public function color($name, $value = null, array $attributes = [])
    {
        return $this->input('color', $name, $value, $attributes);
    }

    /* -----------------------------------------------------------------
     |  Other Methods
     | -----------------------------------------------------------------
     */

    /**
     * Create a checkable input field.
     *
     * @param  string     $type
     * @param  string     $name
     * @param  mixed      $value
     * @param  bool|null  $checked
     * @param  array      $attributes
     *
     * @return \Arcanedev\Html\Elements\Input
     */
    protected function checkable($type, $name, $value, $checked, array $attributes)
    {
        $checked = $this->getCheckedState($type, $name, $value, $checked);

        if ( ! is_null($checked) && $checked)
            $attributes['checked'] = 'checked';

        return $this->input($type, $name, $value, $attributes);
    }

    /**
     * Get the check state for a checkable input.
     *
     * @param  string     $type
     * @param  string     $name
     * @param  mixed      $value
     * @param  bool|null  $checked
     *
     * @return bool
     */
    private function getCheckedState($type, $name, $value, $checked)
    {
        switch($type) {
            case 'checkbox':
                return $this->getCheckboxCheckedState($name, $value, $checked);

            case 'radio':
                return $this->getRadioCheckedState($name, $value, $checked);

            default:
                return $this->getValueAttribute($name) === $value;
        }
    }

    /**
     * Get the check state for a checkbox input.
     *
     * @param  string     $name
     * @param  mixed      $value
     * @param  bool|null  $checked
     *
     * @return bool
     */
    private function getCheckboxCheckedState($name, $value, $checked)
    {
        if (
            isset($this->session) &&
            ! $this->oldInputIsEmpty() &&
            is_null($this->old($name))
        )
            return false;

        if ($this->missingOldAndModel($name))
            return $checked;

        $posted = $this->getValueAttribute($name, $checked);

        if (is_array($posted))
            return in_array($value, $posted);

        if ($posted instanceof Collection)
            return $posted->contains('id', $value);

        return (bool) $posted;
    }

    /**
     * Get the check state for a radio input.
     *
     * @param  string     $name
     * @param  mixed      $value
     * @param  bool|null  $checked
     *
     * @return bool
     */
    private function getRadioCheckedState($name, $value, $checked)
    {
        return $this->missingOldAndModel($name)
            ? $checked
            : $this->getValueAttribute($name) === $value;
    }

    /**
     * Determine if old input or model input exists for a key.
     *
     * @param  string  $name
     *
     * @return bool
     */
    private function missingOldAndModel($name)
    {
        return is_null($this->old($name))
            && is_null($this->getModelValueAttribute($name));
    }

    /**
     * Get the form action from the options.
     *
     * @param  array  $attributes
     *
     * @return string
     */
    private function getAction(array $attributes)
    {
        if (isset($attributes['url']))
            return $this->getUrlAction($attributes['url']);

        if (isset($attributes['route']))
            return $this->getRouteAction($attributes['route']);

        if (isset($attributes['action']))
            return $this->getControllerAction($attributes['action']);

        return $this->url->current();
    }

    /**
     * Get the action for a "url" option.
     *
     * @param  array|string  $attribute
     *
     * @return string
     */
    private function getUrlAction($attribute)
    {
        return is_array($attribute)
            ? $this->url->to($attribute[0], array_slice($attribute, 1))
            : $this->url->to($attribute);
    }

    /**
     * Get the action for a "route" option.
     *
     * @param  array|string  $attribute
     *
     * @return string
     */
    private function getRouteAction($attribute)
    {
        return is_array($attribute)
            ? $this->url->route($attribute[0], array_slice($attribute, 1))
            : $this->url->route($attribute);
    }

    /**
     * Get the action for an "action" option.
     *
     * @param  array|string  $attribute
     *
     * @return string
     */
    private function getControllerAction($attribute)
    {
        return is_array($attribute)
            ? $this->url->action($attribute[0], array_slice($attribute, 1))
            : $this->url->action($attribute);
    }
}


<?php

/*
 * This file is part of the stream package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Stream\Structures;

/**
 */
class Buffer implements \ArrayAccess, \Countable, \IteratorAggregate
{
    /**
     * @var string
     */
    private $data;

    /**
     * Intialize buffer with the given string.
     *
     * @param string $data
     */
    public function __construct(string $data = '')
    {
        $this->data = $data;
    }

    /**
     * Current length of the buffer.
     *
     * @return int
     */
    public function getLength(): int
    {
        return strlen($this->data);
    }

    /**
     * @return int
     */
    public function count(): int
    {
        return $this->getLength();
    }

    /**
     * Determines if the buffer is empty.
     *
     * @return bool
     */
    public function isEmpty(): bool
    {
        return '' === $this->data;
    }

    /**
     * Pushes the given string onto the end of the buffer.
     *
     * @param string $data
     */
    public function push(string $data)
    {
        $this->data .= $data;
    }

    /**
     * Puts the given string at the beginning of the buffer.
     *
     * @param string $data
     */
    public function unshift(string $data)
    {
        $this->data = $data . $this->data;
    }

    /**
     * @param int $length
     *
     * @return string
     */
    public function shift(int $length): string
    {
        $length = (int) $length;
        if (0 >= $length) {
            return '';
        }

        if (strlen($this->data) <= $length) {
            $buffer = $this->data;
            $this->data = '';
            return $buffer;
        }

        $buffer = (string) substr($this->data, 0, $length);

        $this->data = (string) substr($this->data, $length);

        return $buffer;
    }

    /**
     * Returns the given number of characters (at most) from the buffer without removing them from the buffer.
     *
     * @param int $length
     * @param int $offset
     *
     * @return string
     */
    public function peek(int $length, int $offset = 0): string
    {
        $length = (int) $length;
        if (0 >= $length) {
            return '';
        }

        $offset = (int) $offset;
        if (0 > $offset) {
            $offset = 0;
        }

        if (0 === $offset && strlen($this->data) <= $length) {
            return $this->data;
        }

        $result = (string) substr($this->data, $offset, $length);

        return $result;
    }

    /**
     * @param int $length
     *
     * @return string
     */
    public function pop(int $length): string
    {
        $length = (int) $length;
        if (0 >= $length) {
            return '';
        }

        $buffer = (string) substr($this->data, -$length);

        $this->data = (string) substr($this->data, 0, -$length);

        return $buffer;
    }

    /**
     * Removes and returns the given number of characters (at most) from the buffer.
     *
     * @param int $length
     * @param int $offset
     *
     * @return string
     */
    public function remove(int $length, int $offset = 0): string
    {
        $length = (int) $length;
        if (0 >= $length) {
            return '';
        }

        $offset = (int) $offset;
        if (0 > $offset) {
            $offset = 0;
        }

        $buffer = (string) substr($this->data, $offset, $length);

        if (0 === $offset) {
            $this->data = (string) substr($this->data, $length);
        } else {
            $this->data = (string) (substr($this->data, 0, $offset) . substr($this->data, $offset + $length));
        }

        return $buffer;
    }

    /**
     * Removes and returns all data in the buffer.
     *
     * @return string
     */
    public function drain(): string
    {
        $buffer = $this->data;
        $this->data = '';
        return $buffer;
    }

    /**
     * Inserts the string at the given position in the buffer.
     *
     * @param string $string
     * @param int $position
     */
    public function insert(string $string, int $position)
    {
        $this->data = substr_replace($this->data, $string, $position, 0);
    }

    /**
     * Replaces all occurences of $search with $replace. See str_replace() function.
     *
     * @param mixed $search
     * @param mixed $replace
     *
     * @return int Number of replacements performed.
     */
    public function replace($search, $replace): int
    {
        $this->data = str_replace($search, $replace, $this->data, $count);

        return $count;
    }

    /**
     * Returns the position of the given pattern in the buffer if it exists, or false if it does not.
     *
     * @param string $string String to search for.
     * @param bool $reverse Start search from end of buffer.
     *
     * @return int|bool
     *
     * @see strpos()
     */
    public function search($string, $reverse = false)
    {
        if ($reverse) {
            return strrpos($this->data, $string);
        }

        return strpos($this->data, $string);
    }

    /**
     * Determines if the buffer contains the given position.
     *
     * @param int $index
     *
     * @return bool
     */
    public function offsetExists($index): bool
    {
        return isset($this->data[$index]);
    }

    /**
     * Returns the character in the buffer at the given position.
     *
     * @param int $index
     *
     * @return string
     */
    public function offsetGet($index)
    {
        return $this->data[$index];
    }

    /**
     * Replaces the character in the buffer at the given position with the given string.
     *
     * @param int $index
     * @param string $data
     */
    public function offsetSet($index, $data)
    {
        $this->data = substr_replace($this->data, $data, $index, 1);
    }

    /**
     * Removes the character at the given index from the buffer.
     *
     * @param int $index
     */
    public function offsetUnset($index)
    {
        if (isset($this->data[$index])) {
            $this->data = substr_replace($this->data, null, $index, 1);
        }
    }

    /**
     * @return BufferIterator
     */
    public function getIterator(): \Iterator
    {
        return new BufferIterator($this);
    }

    /**
     * @return string
     */
    public function __toString(): string
    {
        return $this->data;
    }
}


<?php

/*
 * This file is part of the stream package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Stream\Pipe;

use Icicle\Awaitable\{Delayed, Exception\TimeoutException};
use Icicle\Exception\InvalidArgumentError;
use Icicle\Loop;
use Icicle\Loop\Watcher\Io;
use Icicle\Stream\Exception\{FailureException, UnreadableException};
use Icicle\Stream\{ReadableStream, StreamResource};

class ReadablePipe extends StreamResource implements ReadableStream
{
    /**
     * Queue to hold \Icicle\Awaitable\Delayed objects.
     *
     * @var \SplQueue
     */
    private $queue;

    /**
     * @var bool
     */
    private $readable = true;

    /**
     * @var \Icicle\Loop\Watcher\Io
     */
    private $poll;

    /**
     * @var \Closure
     */
    private $onCancelled;

    /**
     * @var string
     */
    private $buffer = '';

    /**
     * @param resource $resource Stream resource.
     * @param bool $autoClose True to close the resource on destruct, false to leave it open.
     */
    public function __construct($resource, bool $autoClose = true)
    {
        parent::__construct($resource, $autoClose);

        stream_set_read_buffer($resource, 0);
        stream_set_chunk_size($resource, self::CHUNK_SIZE);

        $this->queue = new \SplQueue();
        $this->poll = $this->createPoll($resource, $this->queue);

        $this->onCancelled = function () {
            $this->poll->cancel();
            $this->queue->shift();
        };
    }

    /**
     * Frees resources associated with this object from the loop.
     */
    public function __destruct()
    {
        parent::__destruct();
        $this->poll->free();
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        parent::close();
        $this->free();
    }

    /**
     * Frees all resources used by the writable stream.
     */
    private function free()
    {
        $this->readable = false;

        $this->poll->free();

        while (!$this->queue->isEmpty()) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $this->queue->shift();
            $delayed->resolve();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function read(int $length = 0, string $byte = null, float $timeout = 0): \Generator
    {
        while (!$this->queue->isEmpty()) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $this->queue->bottom();
            yield $delayed; // Wait for previous read to complete.
        }

        if (!$this->isReadable()) {
            throw new UnreadableException('The stream is no longer readable.');
        }

        if (0 > $length) {
            throw new InvalidArgumentError('The length must be a non-negative integer.');
        }

        if (0 === $length) {
            $length = self::CHUNK_SIZE;
        }

        $byte = strlen($byte) ? $byte[0] : null;

        $resource = $this->getResource();

        while ('' === ($data = $this->fetch($resource, $length, $byte))) {
            if ($this->eof($resource)) { // Close only if no data was read and at EOF.
                $this->free();
                break; // Resolve with empty string on EOF.
            }

            $this->queue->push($delayed = new Delayed($this->onCancelled));
            $this->poll->listen($timeout);

            yield $delayed;
        }

        return $data;
    }

    /**
     * @coroutine
     *
     * Returns a coroutine fulfilled when there is data available to read in the internal stream buffer. Note that
     * this method does not consider data that may be available in the internal buffer. This method can be used to
     * implement functionality that uses the stream socket resource directly.
     *
     * @param float|int $timeout Number of seconds until the returned coroutine is rejected with a TimeoutException
     *     if no data is received. Use 0 for no timeout.
     *
     * @return \Generator
     *
     * @resolve string Empty string.
     *
     * @throws \Icicle\Awaitable\Exception\TimeoutException If the operation times out.
     * @throws \Icicle\Stream\Exception\FailureException If the stream buffer is not empty.
     * @throws \Icicle\Stream\Exception\UnreadableException If the stream is no longer readable.
     * @throws \Icicle\Stream\Exception\ClosedException If the stream has been closed.
     */
    public function poll(float $timeout = 0): \Generator
    {
        while (!$this->queue->isEmpty()) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $this->queue->bottom();
            yield $delayed; // Wait for previous read to complete.
        }

        if (!$this->isReadable()) {
            throw new UnreadableException('The stream is no longer readable.');
        }

        if ('' !== $this->buffer) {
            throw new FailureException('Stream buffer is not empty. Perform another read before polling.');
        }

        $this->queue->push($delayed = new Delayed($this->onCancelled));
        $this->poll->listen($timeout);

        yield $delayed;

        if ('' !== $this->buffer) {
            throw new FailureException('Data unshifted to stream buffer while polling.');
        }

        return ''; // Resolve with empty string.
    }

    /**
     * Shifts the given data back to the front of the stream and will be the first bytes returned from any pending or
     * subsequent read.
     *
     * @param string $data
     */
    public function unshift(string $data)
    {
        $data = (string) $data;

        if (!strlen($data)) {
            return;
        }

        $this->buffer = $data . $this->buffer;

        if (!$this->queue->isEmpty()) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $this->queue->shift();
            $delayed->resolve();
            $this->poll->cancel();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function isReadable(): bool
    {
        return $this->readable;
    }

    /**
     * {@inheritdoc}
     *
     * @param float|int $timeout Timeout for poll if a read was pending.
     */
    public function rebind(float $timeout = 0)
    {
        $pending = $this->poll->isPending();
        $this->poll->free();

        $this->poll = $this->createPoll($this->getResource(), $this->queue);

        if ($pending) {
            $this->poll->listen($timeout);
        }
    }

    /**
     * Reads data from the stream socket resource based on set length and read-to byte.
     *
     * @param resource $resource
     * @param int $length
     * @param string|null $byte
     *
     * @return string
     */
    private function fetch($resource, int $length = self::CHUNK_SIZE, string $byte = null): string
    {
        $remaining = $length;

        if (('' === $this->buffer || 0 < ($remaining -= strlen($this->buffer))) && is_resource($resource)) {
            // Error reporting suppressed since fread() produces a warning if the stream unexpectedly closes.
            $this->buffer .= @fread($resource, $remaining);
        }

        if (null === $byte || false === ($position = strpos($this->buffer, $byte))) {
            if (strlen($this->buffer) <= $length) {
                $data = $this->buffer;
                $this->buffer = '';
                return $data;
            }

            $position = $length;
        } else {
            ++$position; // Include byte in result.
        }

        $data = (string) substr($this->buffer, 0, $position);
        $this->buffer = (string) substr($this->buffer, $position);
        return $data;
    }

    /**
     * @param resource $resource
     *
     * @return bool
     */
    private function eof($resource): bool
    {
        return (!is_resource($resource) || feof($resource)) && '' === $this->buffer;
    }

    /**
     * @param resource $resource
     * @param \SplQueue $queue
     *
     * @return \Icicle\Loop\Watcher\Io
     */
    private function createPoll($resource, \SplQueue $queue): Io
    {
        return Loop\poll($resource, static function ($resource, bool $expired) use ($queue) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $queue->shift();

            if ($expired) {
                $delayed->reject(new TimeoutException('The connection timed out.'));
                return;
            }

            $delayed->resolve();
        });
    }
}


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

/**
 * User: Evren Yurtesen
 * Date: 09-Jul-16
 * Time: 1:16 AM
 */

namespace Yurtesen\Geonames\Console;

use Closure;
use DB;
use Illuminate\Console\OutputStyle;
use RuntimeException;
use ZipArchive;

trait CommandTrait
{

    /**
     * Buffer size to use when executing SQL statements in number of rows
     * I experimented and 1000 is a very reasonable value. Larvger values
     * (5000+) may cause too many placeholders error in eloquent.
     *
     * @var int
     */
    protected $bufferSize = 1000;

    /**
     * This is used to map continent keys with geoname_id values
     *
     * @var array
     */
    protected $continentCodes = array(
        'AF' => 6255146,
        'AS' => 6255147,
        'EU' => 6255148,
        'NA' => 6255149,
        'OC' => 6255151,
        'SA' => 6255150,
        'AN' => 6255152,
    );

    /**
     * This is the main list of url,filename and table values we will work with
     *
     * @var array
     */
    protected $files = array(
        'timeZones' => array(
            'url' => 'http://download.geonames.org/export/dump/timeZones.txt',
            'filename' => 'timeZones',
            'table' => 'geonames_timezones',
        ),
        'allCountries' => array(
            'url' => 'http://download.geonames.org/export/dump/allCountries.zip',
            'filename' => 'allCountries',
            'table' => 'geonames_geonames'
        ),
        'countryInfo' => array(
            'url' => 'http://download.geonames.org/export/dump/countryInfo.txt',
            'filename' => 'countryInfo',
            'table' => 'geonames_country_infos'
        ),
        'iso-languagecodes' => array(
            'url' => 'http://download.geonames.org/export/dump/iso-languagecodes.txt',
            'filename' => 'iso-languagecodes',
            'table' => 'geonames_iso_language_codes',
        ),
        'alternateNames' => array(
            'url' => 'http://download.geonames.org/export/dump/alternateNames.zip',
            'filename' => 'alternateNames',
            'table' => 'geonames_alternate_names',
        ),
        'hierarchy' => array(
            'url' => 'http://download.geonames.org/export/dump/hierarchy.zip',
            'filename' => 'hierarchy',
            'table' => 'geonames_hierarchies',
        ),
        'admin1CodesASCII' => array(
            'url' => 'http://download.geonames.org/export/dump/admin1CodesASCII.txt',
            'filename' => 'admin1CodesASCII',
            'table' => 'geonames_admin1_codes',
        ),
        'admin2Codes' => array(
            'url' => 'http://download.geonames.org/export/dump/admin2Codes.txt',
            'filename' => 'admin2Codes',
            'table' => 'geonames_admin2_codes',
        ),
        // Todo: figure out how to manage different languages
        'featureCodes' => array(
            'url' => 'http://download.geonames.org/export/dump/featureCodes_en.txt',
            'filename' => 'featureCodes_en',
            'table' => 'geonames_feature_codes',
        )
    );

    /**
     * List of tables which are already truncated for avoiding double truncation
     * in case if we are importing multiple files into same table.
     * @var array
     */
    protected $truncatedTables = array();

    /**
     * Parses the array created from different geonames file lines
     * and converts into key=>value type array
     *
     * @param string $name The config name of the file
     * @param boolean $refresh Set true for truncating table before inserting rows
     *
     */
    protected function parseGeonamesText($name, $refresh = false)
    {
        $fieldsArray = array(
            'allCountries' => function ($row) {
                return array(
                    'geoname_id' => $row[0],
                    'name' => $row[1],
                    'ascii_name' => $row[2] ? $row[2] : null,
                    'alternate_names' => $row[3] ? $row[3] : null,
                    'latitude' => $row[4],
                    'longitude' => $row[5],
                    'feature_class' => $row[6] ? $row[6] : null,
                    'feature_code' => $row[7] ? $row[7] : null,
                    'country_code' => $row[8],
                    'cc2' => $row[9] ? $row[9] : null,
                    'admin1_code' => $row[10] ? $row[10] : null,
                    'admin2_code' => $row[11] ? $row[11] : null,
                    'admin3_code' => $row[12] ? $row[12] : null,
                    'admin4_code' => $row[13] ? $row[13] : null,
                    'population' => $row[14] ? $row[14] : null,
                    'elevation' => $row[15] ? $row[15] : null,
                    'dem' => $row[16] ? $row[16] : null,
                    'timezone_id' => $row[17] ? $row[17] : null,
                    'modified_at' => trim($row[18]),
                );
            },
            'iso-languagecodes' => function ($row) {
                return array(
                    'iso_639_3' => $row[0],
                    'iso_639_2' => $row[1] ? $row[1] : null,
                    'iso_639_1' => $row[2] ? $row[2] : null,
                    'language_name' => trim($row[3]),
                );
            },
            'hierarchy' => function ($row) {
                return array(
                    'parent_id' => $row[0],
                    'child_id' => $row[1],
                    'type' => trim($row[2]),
                );
            },
            'alternateNames' => function ($row) {
                return array(
                    'alternate_name_id' => $row[0],
                    'geoname_id' => $row[1],
                    'iso_language' => $row[2] ? $row[2] : null,
                    'alternate_name' => $row[3] ? $row[3] : null,
                    'isPreferredName' => $row[4] ? 1 : 0,
                    'isShortName' => $row[5] ? 1 : 0,
                    'isColloquial' => $row[6] ? 1 : 0,
                    'isHistoric' => $row[7] ? 1 : 0,
                );
            },
            'timeZones' => function ($row) {
                // Skip unusual comment line in this file
                if ($row[0] === 'CountryCode') return null;
                return array(
                    'country_code' => $row[0],
                    'timezone_id' => $row[1],
                    'gmt_offset' => $row[2],
                    'dst_offset' => $row[3],
                    'raw_offset' => $row[4]
                );
            },
            'countryInfo' => function ($row) {
                return array(
                    'iso' => $row[0],
                    'iso3' => $row[1],
                    'iso_numeric' => $row[2],
                    'fips' => $row[3] ? $row[3] : null,
                    'country' => $row[4] ? $row[4] : null,
                    'capital' => $row[5] ? $row[5] : null,
                    'area' => $row[6] ? $row[6] : null,
                    'population' => $row[7] ? $row[7] : null,
                    'continent_code' => $row[8],
                    'continent_id' => $this->continentCodes[$row[8]],
                    'tld' => $row[9] ? $row[9] : null,
                    'currency_code' => $row[10] ? $row[10] : null,
                    'currency_name' => $row[11] ? $row[11] : null,
                    'phone' => $row[12] ? $row[12] : null,
                    'postal_code_format' => $row[13] ? $row[13] : null,
                    'postal_code_regex' => $row[14] ? $row[14] : null,
                    'languages' => $row[15] ? $row[15] : null,
                    'geoname_id' => $row[16] ? $row[16] : null,
                    'neighbors' => $row[17] ? $row[17] : null,
                    'equivalent_fips_code' => $row[18] ? trim($row[18]) : null,
                );
            },
            'featureCodes' => function ($row) {
                return array(
                    'code' => $row[0],
                    'name' => $row[1],
                    'description' => trim($row[2]),
                );
            },
            'admin1CodesASCII' => function ($row) {
                return array(
                    'code' => $row[0],
                    'name' => $row[1],
                    'name_ascii' => $row[2],
                    'geoname_id' => $row[3],
                );
            },
            'admin2Codes' => function ($row) {
                return array(
                    'code' => $row[0],
                    'name' => $row[1],
                    'name_ascii' => $row[2],
                    'geoname_id' => $row[3],
                );
            },
        );

        // This will greatly improve the performance of inserts
        DB::statement('SET FOREIGN_KEY_CHECKS=0;');
        $tableName = $this->files[$name]['table'];
        // If table is empty or we are refreshing, truncate it unless it was recently truncated!
        if (!in_array($tableName,$this->truncatedTables) &&
            (DB::table($tableName)->count() === 0 || $refresh)
        ) {
            $this->truncatedTables[] = $tableName;
            $this->line('<info>Database:</info> Truncating table ' . $tableName);
            DB::table($tableName)->truncate();
        }
        $buffer = array();
        // If it is a custom country code, use allCountries
        if (in_array($name, config('geonames.countries')))
            $fields = $fieldsArray['allCountries'];
        else
            $fields = $fieldsArray[$name];
        $this->parseFile($name, function ($row) use (&$buffer, $fields, $tableName) {
            $insert = $fields($row);
            if (isset($insert) && is_array($insert)) {
                $buffer[] = $insert;
            }
            if (count($buffer) === $this->bufferSize) {
                $this->updateOrInsertMultiple($tableName, $buffer);
                $buffer = array();
            }
        });

        // Insert leftovers in buffer
        if (count($buffer) > 0)
            $this->updateOrInsertMultiple($tableName, $buffer);

        DB::statement('SET FOREIGN_KEY_CHECKS=1;');
    }

    /**
     * Parse a given file and insert into databaase using closure.
     *
     * @param  string $name
     * @param  Closure $callback
     * @return void
     */
    protected function parseFile($name, Closure $callback)
    {
        $url = $this->files[$name]['url'];
        $basename = basename($url);
        $storagePath = config('geonames.storagePath');
        // Final file path
        $path = $storagePath . '/' . $this->files[$name]['filename'] . '.txt';

        // Check if file exists (should be since we just downloaded them)
        $downloadedFilePath = $storagePath . '/' . $basename;
        if (!file_exists($downloadedFilePath)) {
            throw new RuntimeException('File does not exist: ' . $downloadedFilePath);
        }
        // If it is a zip file we must unzip it
        if (substr($basename, -4) === '.zip') {
            $this->unZip($name);
        }

        $steps = $this->getLineCount($path);

        /* @var $output OutputStyle */
        $output = $this->getOutput();
        $bar = $output->createProgressBar($steps);
        $bar->setFormat('<info>Seeding File:</info> ' . basename($path) . ' %current%/%max% %bar% %percent%% <info>Remaining Time:</info> %remaining%');

        $steps = 0;
        $fh = fopen($path, 'r');
        if (!$fh) {
            throw new RuntimeException("Can not open file: $path");
        }
        while (!feof($fh)) {
            $line = fgets($fh);
            // ignore empty lines and comments
            if (!$line or $line === '' or strpos($line, '#') === 0) continue;
            // Geonames format is tab seperated
            $line = explode("\t", $line);
            // Insert using closure
            $callback($line);
            $steps++;
            if (isset($bar) && $steps % ($this->bufferSize * 10) === 0)
                $bar->advance($this->bufferSize * 10);
        }
        fclose($fh);
        if (isset($bar)) {
            $bar->finish();
            $output->newLine();
        }
        // If we wont keep txt version delete file
        if (substr($basename, -4) === '.zip' && !config('geonames.keepTxt')) {
            $this->line('<info>Removing File:</info> ' . basename($path));
            unlink($path);
        }
    }

    /**
     * Read the file and get line count
     * Not very efficient but does the job well...
     *
     * @param  string $path
     * @return int $count
     */
    protected function getLineCount($path)
    {
        $fh = fopen($path, 'r');
        if (!$fh) {
            throw new RuntimeException("Can not open file: $path");
        }
        $fileSize = @filesize($path);
        /* @var $output OutputStyle */
        $output = $this->getOutput();
        $bar = $output->createProgressBar($fileSize);
        $bar->setFormat('<info>Reading File:</info> ' . basename($path) . ' %bar% %percent%% <info>Remaining Time:</info> %remaining%');

        $steps = 0;
        $currentSize = 0;
        while (!feof($fh)) {
            $line = fgets($fh);
            $currentSize += strlen($line);
            // ignore empty lines and comments
            if (!$line or $line === '' or strpos($line, '#') === 0) continue;
            $steps++;
            // Reading is so much faster, must slow down advances
            if (isset($bar) && $steps % ($this->bufferSize * 100) === 0) {
                $bar->advance($currentSize);
                $currentSize = 0;
            }
        }
        fclose($fh);
        if (isset($bar)) {
            $bar->finish();
            $output->newLine();
        }
        return $steps;
    }

    /**
     * Create and run a single SQL INSERT query for multiple rows of data.
     * $data argument is an array of database row arrays in key=>value pairs.
     * This speeds up inserts ~50 times compared to line-by-line inserts.
     *
     * Note: The $data must be an array of arrays and have at least 2 elements.
     *
     * @param  string $tableName
     * @param  array (array()) $data
     * @return boolean
     */
    protected function updateOrInsertMultiple($tableName, $data)
    {
        $fields = '`' . implode('`,`', array_keys($data[0])) . '`';
        // Create strings for variables
        $questionMarks = '';
        $updateList = '';
        foreach (array_keys($data[0]) as $key) {
            $updateList .= '`' . $key . '`' . '=VALUES(' . '`' . $key . '`' . '),';
            $questionMarks .= '?,';
        }
        // Remove last extra comma
        $updateList = rtrim($updateList, ',');
        $questionMarks = rtrim($questionMarks, ',');

        // If files were not in sync, there may be missing entries in some tables
        $sql = /** @lang text */
            'INSERT IGNORE INTO ' . $tableName . ' (' . $fields . ') ';
        $sql .= 'VALUES ' . PHP_EOL;
        // Set placeholders
        $sql .= '(' . $questionMarks . ')';
        for ($i = 1; $i < count($data); $i++) {
            $sql .= ',' . PHP_EOL . '(' . $questionMarks . ')';
        }
        $sql .= PHP_EOL;
        $valueArray = array();
        foreach ($data as $rows) {
            foreach ($rows as $key => $value) {
                $valueArray[] = $value;
            }
        }
        // Set action on duplicate key updates
        $sql .= "ON DUPLICATE KEY UPDATE ";
        $sql .= $updateList;

        return DB::insert($sql, $valueArray);
    }

    /**
     * Download a file if it does not exist
     *
     * @param Boolean $update Update files
     *
     */
    protected function downloadAllFiles($update = false)
    {
        $files = array_keys($this->getFilesArray());
        foreach ($files as $name) {
            $this->downloadFile($name, $update);
        }
    }

    /**
     * Download a file if it does not exist
     *
     * @param String $url Download URL
     * @param String $path Storage path
     * @param Boolean $force Force re-download of files
     *
     * @return boolean
     */
    protected function downloadFile($name, $update = false)
    {
        $url = $this->files[$name]['url'];
        $storagePath = config('geonames.storagePath');
        $txtFileName = $this->files[$name]['filename'] . '.txt';
        $urlSize = $this->getUrlSize($url);
        $urlFileName = basename($url);

        $fileSize = @filesize($storagePath . '/' . $urlFileName);
        if ($fileSize) {
            if ($fileSize === $urlSize) {
                $this->line('<info>File Exists:</info> ' . $urlFileName . ' with same size as remote geonames file exists.');
                return true;
            } else if ($fileSize !== $urlSize && !$update) {
                $this->line('<info>File Exists:</info> ' . $urlFileName . ' with different size as remote geonames file exists. You should consider downloading newest files.');
                return true;
            } else {
                // If we are here, we will re-download zip file, so it is worthwhile to remoeve the old txt version first
                $extractedFilePath = $storagePath . '/' . $txtFileName;
                if (substr($urlFileName, -4) === '.zip' && file_exists($extractedFilePath)) {
                    $this->line('<info>Removing Old File:</info> ' . basename($extractedFilePath));
                    unlink($extractedFilePath);
                }
            }
        }

        // Create download directory
        if (!file_exists($storagePath)) {
            mkdir($storagePath, 0755, true);
        }

        // Create .gitignore if it does not exist in $storagePath
        if (!file_exists($storagePath.'/.gitignore')) {
            $gitignore = "*\n!.gitignore\n";
            file_put_contents($storagePath.'/.gitignore',$gitignore);
        }

        // Open file and truncate it for writing (requires fopen_wrappers)
        $targetFile = $storagePath . '/' . $urlFileName;
        $targetFP = fopen($targetFile, 'w+');
        if ($targetFP === false) {
            throw new RuntimeException('Can not open file for writing: ' . $targetFile);
        }

        $sourceFP = fopen($url, 'r');
        if ($sourceFP === false) {
            throw new RuntimeException('Can not open file for reading: ' . $url);
        }

        $bufferSize = 1024 * 1024;
        // Size should be at leaast 1 to show remaining time...
        $steps = $urlSize / $bufferSize + 1;
        /* @var $output OutputStyle */
        $output = $this->getOutput();
        $bar = $output->createProgressBar($steps);
        $bar->setFormat('<info>Downloading:</info> ' . $urlFileName . ' %bar% %percent%% <info>Remaining Time:</info> %remaining%');
        while (!feof($sourceFP)) {
            fwrite($targetFP, stream_get_contents($sourceFP, $bufferSize));
            $bar->advance();
        }
        $bar->finish();
        $output->newLine();

        clearstatcache(true, $targetFile);
        $this->line('<info>File Downloaded:</info> ' . $urlFileName . ' - ' . filesize($targetFile) . ' bytes.');

        return true;
    }

    /**
     * Unzip the file
     *
     * @param  string $name
     */
    protected function unZip($name)
    {
        $zipFileName = basename($this->files[$name]['url']);
        if (!substr($zipFileName, -4) === '.zip')
            throw new RuntimeException($zipFileName . ' does not have .zip extension');

        // Final file path
        $storagePath = config('geonames.storagePath');
        $extractedFile = $this->files[$name]['filename'] . '.txt';
        $path = $storagePath . '/' . $extractedFile;

        // Open zip archive because we need the size of extracted file
        $zipArchive = new ZipArchive;
        $zipArchive->open($storagePath . '/' . $zipFileName);

        if (file_exists($path)) {
            $uncompressedSize = $zipArchive->statName($extractedFile)['size'];
            $fileSize = filesize($path);
            if ($uncompressedSize !== $fileSize) {
                $this->line('<info>Existing File:</info> ' . basename($path) . ' size does not match the one in ' . $zipFileName);
            } else {
                // Do not extract again
                $this->line('<info>Existing File:</info> ' . 'Found ' . basename($path) . ' file extracted from ' . $zipFileName);
                $zipArchive->close();
                return;
            }
        }
        // File does not exist or size does not match
        $this->line('<info>Extracting File:</info> ' . $extractedFile . ' from ' . $zipFileName . ' ...!!!Please Wait!!!...');
        // Extract file
        $zipArchive->extractTo($storagePath . '/', $extractedFile);
        $zipArchive->close();
    }

    /**
     * Get Remote File Size
     *
     * @param string $url remote address
     * @return int|boolean URL size in bytes or false
     */
    protected function getUrlSize($url)
    {
        $data = get_headers($url, true);
        if (isset($data['Content-Length']))
            return (int)$data['Content-Length'];
        return false;
    }

    /**
     * Returns files array after removing entries in
     * ignoreTables config option and adding custom countries from
     * countries config option if necessary
     *
     * @return array
     */
    protected function getFilesArray()
    {
        static $firstRun = true;
        if ($firstRun) {
            $this->updateFilesList();
            $firstRun = false;
        }
        $data = $this->files;
        foreach ($data as $key => $value) {
            if (in_array($value['table'], config('geonames.ignoreTables'))) {
                unset($data[$key]);
            }
        }
        return $data;
    }

    protected function updateFilesList()
    {
        // Get ISO codes for countries to import if there are any
        $countries = config('geonames.countries');
        if (!empty($countries)) {

            unset($this->files['allCountries']);
            foreach ($countries as $country) {
                $this->files[$country] = [
                    'url' => 'http://download.geonames.org/export/dump/' . $country . '.zip',
                    'filename' => $country,
                    'table' => 'geonames_geonames'
                ];
            }
        }
    }
}


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

/**
 * User: Evren Yurtesen
 * Date: 04-Jul-16
 * Time: 5:14 PM
 */

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateGeonamesCountryInfosTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('geonames_country_infos', function (Blueprint $table) {
            $table->char('iso',2)->unique();
            $table->char('iso3',3)->unique();
            $table->char('iso_numeric',3)->unique();
            //$table->smallInteger('iso_numeric')->unsigned();
            $table->char('fips',2)->nullable();
            $table->string('country',60);
            $table->string('capital',40);
            $table->integer('area')->unsigned();
            $table->integer('population')->unsigned()->nullable();
            $table->char('continent_code',2);
            $table->integer('continent_id')->unsigned();
            $table->foreign('continent_id')->references('geoname_id')->on('geonames_geonames')->onUpdate('cascade')->onDelete('cascade');
            $table->char('tld',3)->nullable();
            $table->char('currency_code',3)->nullable();
            $table->string('currency_name',20)->nullable();
            $table->string('phone',20)->nullable();
            $table->string('postal_code_format',100)->nullable();
            $table->string('postal_code_regex',200)->nullable();
            $table->string('languages',100)->nullable();
            $table->integer('geoname_id')->primary()->unsigned();
            $table->foreign('geoname_id')->references('geoname_id')->on('geonames_geonames')->onUpdate('cascade')->onDelete('cascade');
            $table->string('neighbors',60)->nullable();
            $table->char('equivalent_fips_code',2)->nullable();
        });
        //DB::statement('ALTER TABLE geonames_country_infos CHANGE iso_numeric iso_numeric SMALLINT(3) UNSIGNED ZEROFILL NOT NULL');

        // Now can add the foreign key constraint to timezones table also
        Schema::table('geonames_timezones', function (Blueprint $table) {
            $table->foreign('country_code')->references('iso')->on('geonames_country_infos')->onUpdate('cascade')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        // First drop the foreign constraint from timezones table
        Schema::table('geonames_timezones', function (Blueprint $table) {
            $table->dropForeign('geonames_timezones_country_code_foreign');
        });
        Schema::drop('geonames_country_infos');
    }
}


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

/**
 * User: Evren Yurtesen
 * Date: 04-Jul-16
 * Time: 5:14 PM
 */

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateGeonamesAlternateNamesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('geonames_alternate_names', function (Blueprint $table) {
            $table->integer('alternate_name_id')->primary()->unsigned();
            $table->integer('geoname_id')->index()->unsigned();
            $table->foreign('geoname_id')->references('geoname_id')->on('geonames_geonames')->onUpdate('cascade')->onDelete('cascade');
            $table->string('iso_language', 7)->nullable();
            $table->string('alternate_name', 400)->nullable();
            $table->boolean('isPreferredName');
            $table->boolean('isShortName');
            $table->boolean('isColloquial');
            $table->boolean('isHistoric');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('geonames_alternate_names');
    }
}


<?php

/*
 * This file is part of the stream package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Stream\Text;

use Icicle\Stream\{Structures\Buffer, WritableStream};

/**
 * Writes text to a stream.
 */
class TextWriter
{
    const DEFAULT_BUFFER_SIZE = 16384;

    /**
     * @var \Icicle\Stream\WritableStream The stream to write to.
     */
    private $stream;

    /**
     * @var \Icicle\Stream\Structures\Buffer
     */
    private $buffer;

    /**
     * @var float The timeout for write operations. Use 0 for no timeout.
     */
    private $timeout;

    /**
     * @var bool Indicates if the buffer should be flushed on every write.
     */
    private $autoFlush = false;

    /**
     * @var int The max buffer size in bytes.
     */
    private $bufferSize;

    /**
     * Creates a new stream writer for a given stream.
     *
     * @param \Icicle\Stream\WritableStream $stream The stream to write to.
     * @param float|int $timeout The timeout for write operations. Use 0 for no timeout.
     * @param string $encoding
     * @param bool $autoFlush Indicates if the buffer should be flushed on every write.
     * @param int $bufferSize The max buffer size in bytes.
     */
    public function __construct(
        WritableStream $stream,
        float $timeout = 0,
        string $encoding = 'UTF-8',
        bool $autoFlush = false,
        int $bufferSize = self::DEFAULT_BUFFER_SIZE
    ) {
        $this->stream = $stream;
        $this->buffer = new Buffer();
        $this->timeout = $timeout;
        $this->autoFlush = $autoFlush;
        $this->bufferSize = $bufferSize;
        $this->newLine = mb_convert_encoding("\n", $encoding, 'UTF-8');
    }

    /**
     * Gets the underlying stream.
     *
     * @return \Icicle\Stream\WritableStream
     */
    public function getStream(): WritableStream
    {
        return $this->stream;
    }

    /**
     * Determines if the stream is still open.
     *
     * @return bool
     */
    public function isOpen(): bool
    {
        return $this->stream->isOpen();
    }

    /**
     * Closes the stream writer and the underlying stream.
     *
     * The buffer will not be automatically flushed. You should call `flush()` before
     * closing to ensure all data is written to the stream.
     */
    public function close()
    {
        $this->stream->close();
    }

    /**
     * @coroutine
     *
     * Flushes the contents of the internal buffer to the underlying stream.
     *
     * @return \Generator
     *
     * @resolve int Number of bytes written to the stream.
     *
     * @throws \Icicle\Awaitable\Exception\TimeoutException If the operation times out.
     * @throws \Icicle\Stream\Exception\UnwritableException If the stream is no longer writable.
     * @throws \Icicle\Stream\Exception\ClosedException If the stream is unexpectedly closed.
     */
    public function flush(): \Generator
    {
        if ($this->buffer->isEmpty()) {
            return 0;
        }

        return yield from $this->stream->write($this->buffer->drain(), $this->timeout);
    }

    /**
     * @coroutine
     *
     * Writes a value to the stream.
     *
     * The given value will be coerced to a string before being written. The resulting
     * string will be written to the internal buffer; if the buffer is full, the entire
     * buffer will be flushed to the stream.
     *
     * @param mixed $text A printable value that can be coerced to a string.
     *
     * @return \Generator
     *
     * @resolve int Number of bytes written to the buffer.
     *
     * @throws \Icicle\Awaitable\Exception\TimeoutException If the operation times out.
     * @throws \Icicle\Stream\Exception\UnwritableException If the stream is no longer writable.
     * @throws \Icicle\Stream\Exception\ClosedException If the stream is unexpectedly closed.
     */
    public function write(string $text): \Generator
    {
        $length = strlen($text);

        $this->buffer->push($text);

        if ($this->autoFlush || $this->buffer->getLength() > $this->bufferSize) {
            yield from $this->flush();
        }

        return $length;
    }

    /**
     * @coroutine
     *
     * Writes a value to the stream and then terminates the line.
     *
     * The given value will be coerced to a string before being written.
     *
     * @param mixed $text A printable value that can be coerced to a string.
     *
     * @return \Generator
     *
     * @resolve int Number of bytes written to the buffer.
     *
     * @throws \Icicle\Awaitable\Exception\TimeoutException If the operation times out.
     * @throws \Icicle\Stream\Exception\UnwritableException If the stream is no longer writable.
     * @throws \Icicle\Stream\Exception\ClosedException If the stream is unexpectedly closed.
     */
    public function writeLine(string $text): \Generator
    {
        return yield from $this->write($text . $this->newLine, $this->timeout);
    }

    /**
     * @coroutine
     *
     * Writes a formatted string to the stream.
     *
     * Accepts a format string followed by a series of mixed values. The string
     * will be formatted in accordance with the specification of the built-in
     * function `printf()`.
     *
     * @param string $format The format string.
     *
     * @return \Generator
     *
     * @resolve int Number of bytes written to the buffer.
     *
     * @throws \Icicle\Awaitable\Exception\TimeoutException If the operation times out.
     * @throws \Icicle\Stream\Exception\UnwritableException If the stream is no longer writable.
     * @throws \Icicle\Stream\Exception\ClosedException If the stream is unexpectedly closed.
     *
     * @see http://php.net/printf
     */
    public function printf(string $format, ...$args): \Generator
    {
        $formatted = sprintf($format, ...$args);
        return yield from $this->write($formatted);
    }

    /**
     * @coroutine
     *
     * Writes a formatted string to the stream and then terminates the line.
     *
     * Accepts a format string followed by a series of mixed values. The string
     * will be formatted in accordance with the specification of the built-in
     * function `printf()`.
     *
     * @param string $format The format string.
     *
     * @return \Generator
     *
     * @resolve int Number of bytes written to the buffer.
     *
     * @throws \Icicle\Awaitable\Exception\TimeoutException If the operation times out.
     * @throws \Icicle\Stream\Exception\UnwritableException If the stream is no longer writable.
     * @throws \Icicle\Stream\Exception\ClosedException If the stream is unexpectedly closed.
     *
     * @see http://php.net/printf
     */
    public function printLine(string $format, ...$args): \Generator
    {
        $formatted = sprintf($format, ...$args);
        yield $this->write($formatted . $this->newLine);
    }
}


<?php

/*
 * This file is part of the stream package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Stream;

use Icicle\Awaitable\Delayed;
use Icicle\Exception\InvalidArgumentError;
use Icicle\Stream\Exception\{ClosedException, UnreadableException, UnwritableException};

/**
 * Serves as buffer that implements the stream interface, allowing consumers to be notified when data is available in
 * the buffer. This class by itself is not particularly useful, but it can be extended to add functionality upon reading
 * or writing, as well as acting as an example of how stream classes can be implemented.
 */
class MemoryStream implements DuplexStream
{
    /**
     * @var \Icicle\Stream\Structures\Buffer
     */
    private $buffer;

    /**
     * @var bool
     */
    private $readable = true;

    /**
     * @var bool
     */
    private $writable = true;

    /**
     * @var \Icicle\Awaitable\Delayed|null
     */
    private $delayed;

    /**
     * @var int
     */
    private $length;

    /**
     * @var string|null
     */
    private $byte;

    /**
     * @var int
     */
    private $hwm;

    /**
     * @var \SplQueue|null
     */
    private $queue;

    /**
     * @var \Closure|null
     */
    private $onCancelled;

    /**
     * @param int $hwm High water mark. If the internal buffer has more than $hwm bytes, writes to the stream will
     *     return pending promises until the data is consumed.
     * @param string $data
     */
    public function __construct(int $hwm = 0, string $data = '')
    {
        $this->buffer = new Structures\Buffer($data);
        $this->hwm = (int) $hwm;
        if (0 > $this->hwm) {
            $this->hwm = 0;
        }

        if (0 !== $this->hwm) {
            $this->queue = new \SplQueue();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function isOpen(): bool
    {
        return $this->readable || $this->writable;
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        $this->free();
    }

    /**
     * Closes the stream and rejects any pending promises.
     *
     * @param \Throwable|null $exception
     */
    protected function free(\Throwable $exception = null)
    {
        $this->readable = false;
        $this->writable = false;

        if (null !== $this->delayed) {
            $this->delayed->resolve('');
        }

        if (0 !== $this->hwm) {
            while (!$this->queue->isEmpty()) {
                /** @var \Icicle\Awaitable\Delayed $delayed */
                $delayed = $this->queue->shift();
                $delayed->reject(
                    $exception = $exception ?: new ClosedException('The stream was unexpectedly closed.')
                );
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function read(int $length = 0, string $byte = null, float $timeout = 0): \Generator
    {
        while (null !== $this->delayed) {
            yield $this->delayed;
        }

        if (!$this->isReadable()) {
            throw new UnreadableException('The stream is no longer readable.');
        }

        $this->length = $length;
        if (0 > $this->length) {
            throw new InvalidArgumentError('The length should be a positive integer.');
        }

        $this->byte = strlen($byte) ? $byte[0] : null;

        if (!$this->buffer->isEmpty()) {
            $data = $this->remove();

            if (0 !== $this->hwm && $this->buffer->getLength() <= $this->hwm) {
                while (!$this->queue->isEmpty()) {
                    /** @var \Icicle\Awaitable\Delayed $delayed */
                    $delayed = $this->queue->shift();
                    $delayed->resolve();
                }
            }

            if (!$this->writable && $this->buffer->isEmpty()) {
                $this->free();
            }

            return $data;
        }

        $awaitable = $this->delayed = new Delayed();

        if ($timeout) {
            $awaitable = $this->delayed->timeout($timeout);
        }

        try {
            $data = yield $awaitable;
        } finally {
            $this->delayed = null;
        }

        return $data;
    }

    /**
     * Returns bytes from the buffer based on the current length or current search byte.
     *
     * @return string
     */
    private function remove(): string
    {
        if (null !== $this->byte && false !== ($position = $this->buffer->search($this->byte))) {
            if (0 === $this->length || $position < $this->length) {
                return $this->buffer->shift($position + 1);
            }

            return $this->buffer->shift($this->length);
        }

        if (0 === $this->length) {
            return $this->buffer->drain();
        }

        return $this->buffer->shift($this->length);
    }

    /**
     * Shifts the given data back to the front of the stream and will be the first bytes returned from any pending or
     * subsequent read.
     *
     * @param string $data
     */
    public function unshift(string $data)
    {
        if (!strlen($data)) {
            return;
        }

        $this->buffer->unshift($data);

        if (null !== $this->delayed && !$this->buffer->isEmpty()) {
            $delayed = $this->delayed;
            $this->delayed = null;
            $delayed->resolve($this->remove());
        }
    }

    /**
     * {@inheritdoc}
     */
    public function isReadable(): bool
    {
        return $this->readable;
    }

    /**
     * {@inheritdoc}
     */
    public function write(string $data, float $timeout = 0): \Generator
    {
        return $this->send($data, $timeout, false);
    }

    /**
     * {@inheritdoc}
     */
    public function end(string $data = '', float $timeout = 0): \Generator
    {
        return $this->send($data, $timeout, true);
    }

    /**
     * @coroutine
     *
     * @param string $data
     * @param float|int $timeout
     * @param bool $end
     *
     * @return \Generator
     *
     * @resolve int Number of bytes written to the stream.
     *
     * @throws \Icicle\Stream\Exception\UnwritableException If the stream is no longer writable.
     */
    protected function send(string $data, float $timeout = 0, bool $end = false): \Generator
    {
        if (!$this->isWritable()) {
            throw new UnwritableException('The stream is no longer writable.');
        }

        $this->buffer->push($data);

        if (null !== $this->delayed && !$this->buffer->isEmpty()) {
            $delayed = $this->delayed;
            $this->delayed = null;
            $delayed->resolve($this->remove());
        }

        if ($end) {
            if ($this->buffer->isEmpty()) {
                $this->free();
            } else {
                $this->writable = false;
            }
        }

        if (0 !== $this->hwm && $this->buffer->getLength() > $this->hwm) {
            $awaitable = new Delayed($this->onCancelled = $this->onCancelled ?: function () {
                $this->free();
            });
            $this->queue->push($awaitable);

            if ($timeout) {
                $awaitable = $awaitable->timeout($timeout);
            }

            yield $awaitable;
        }

        return strlen($data);
    }

    /**
     * {@inheritdoc}
     */
    public function isWritable(): bool
    {
        return $this->writable;
    }
}


<?php

/*
 * This file is part of the stream package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Stream\Structures;

use Icicle\Stream\Exception\OutOfBoundsException;

/**
 */
class BufferIterator implements \SeekableIterator
{
    /**
     * @var Buffer
     */
    private $buffer;

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

    /**
     * @param \Icicle\Stream\Structures\Buffer $buffer
     */
    public function __construct(Buffer $buffer)
    {
        $this->buffer = $buffer;
    }

    /**
     * Rewinds the iterator to the beginning of the buffer.
     */
    public function rewind()
    {
        $this->current = 0;
    }

    /**
     * Determines if the iterator is valid.
     *
     * @return bool
     */
    public function valid(): bool
    {
        return isset($this->buffer[$this->current]);
    }

    /**
     * Returns the current position (key) of the iterator.
     *
     * @return int
     */
    public function key(): int
    {
        return $this->current;
    }

    /**
     * Returns the current character in the buffer at the iterator position.
     *
     * @return string
     */
    public function current(): string
    {
        return $this->buffer[$this->current];
    }

    /**
     * Moves to the next character in the buffer.
     */
    public function next()
    {
        ++$this->current;
    }

    /**
     * Moves to the previous character in the buffer.
     */
    public function prev()
    {
        --$this->current;
    }

    /**
     * Moves to the given position in the buffer.
     *
     * @param int $position
     */
    public function seek($position)
    {
        $position = (int) $position;
        if (0 > $position) {
            $position = 0;
        }

        $this->current = $position;
    }

    /**
     * Inserts the given string into the buffer at the current iterator position.
     *
     * @param string $data
     *
     * @throws \Icicle\Stream\Exception\OutOfBoundsException If the iterator is invalid.
     */
    public function insert(string $data)
    {
        if (!$this->valid()) {
            throw new OutOfBoundsException('The iterator is not valid!');
        }

        $this->buffer[$this->current] = $data . $this->buffer[$this->current];
    }

    /**
     * Replaces the byte at the current iterator position with the given string.
     *
     * @param string $data
     *
     * @return string
     *
     * @throws \Icicle\Stream\Exception\OutOfBoundsException If the iterator is invalid.
     */
    public function replace(string $data): string
    {
        if (!$this->valid()) {
            throw new OutOfBoundsException('The iterator is not valid!');
        }

        $temp = $this->buffer[$this->current];

        $this->buffer[$this->current] = $data;

        return $temp;
    }

    /**
     * Removes the byte at the current iterator position and moves the iterator to the previous character.
     *
     * @return string
     *
     * @throws \Icicle\Stream\Exception\OutOfBoundsException If the iterator is invalid.
     */
    public function remove(): string
    {
        if (!$this->valid()) {
            throw new OutOfBoundsException('The iterator is not valid!');
        }

        $temp = $this->buffer[$this->current];

        unset($this->buffer[$this->current]);

        --$this->current;

        return $temp;
    }
}


<?php

/*
 * This file is part of the stream package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Stream\Pipe;

use Icicle\Stream\{DuplexStream, Exception\UnwritableException, Resource};

class DuplexPipe implements DuplexStream, Resource
{
    /**
     * @var \Icicle\Stream\Pipe\ReadablePipe
     */
    private $readable;

    /**
     * @var \Icicle\Stream\Pipe\WritablePipe
     */
    private $writable;

    /**
     * @param resource $resource Stream socket resource.
     * @param bool $autoClose True to close the resource on destruct, false to leave it open.
     */
    public function __construct($resource, bool $autoClose = true)
    {
        $this->readable = new ReadablePipe($resource, $autoClose);
        $this->writable = new WritablePipe($resource, $autoClose);
    }

    /**
     * {@inheritdoc}
     */
    public function isOpen(): bool
    {
        return $this->readable->isOpen() || $this->writable->isOpen();
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        $this->readable->close();
        $this->writable->close();
    }

    /**
     * {@inheritdoc}
     */
    public function getResource()
    {
        return $this->readable->getResource();
    }

    /**
     * {@inheritdoc}
     */
    public function read(int $length = 0, string $byte = null, float $timeout = 0): \Generator
    {
        return $this->readable->read($length, $byte, $timeout);
    }

    /**
     * @coroutine
     *
     * Returns a coroutine fulfilled when there is data available to read in the internal stream buffer. Note that
     * this method does not consider data that may be available in the internal buffer. This method should be used to
     * implement functionality that uses the stream socket resource directly.
     *
     * @param float|int $timeout Number of seconds until the returned promise is rejected with a TimeoutException
     *     if no data is received. Use null for no timeout.
     *
     * @return \Generator
     *
     * @resolve string Empty string.
     *
     * @throws \Icicle\Awaitable\Exception\TimeoutException If the operation times out.
     * @throws \Icicle\Stream\Exception\FailureException If the stream buffer is not empty.
     * @throws \Icicle\Stream\Exception\UnreadableException If the stream is no longer readable.
     * @throws \Icicle\Stream\Exception\ClosedException If the stream has been closed.
     */
    public function poll(float $timeout = 0): \Generator
    {
        return $this->readable->poll($timeout);
    }

    /**
     * {@inheritdoc}
     */
    public function isReadable(): bool
    {
        return $this->readable->isReadable();
    }

    /**
     * Shifts the given data back to the front of the stream and will be the first bytes returned from any pending or
     * subsequent read.
     *
     * @param string $data
     */
    public function unshift(string $data)
    {
        $this->readable->unshift($data);
    }

    /**
     * {@inheritdoc}
     */
    public function write(string $data, float $timeout = 0): \Generator
    {
        return $this->writable->write($data, $timeout);
    }

    /**
     * {@inheritdoc}
     */
    public function end(string $data = '', float $timeout = 0): \Generator
    {
        if (!$this->writable->isWritable()) {
            throw new UnwritableException('The stream is no longer writable.');
        }

        try {
            $written = yield from $this->writable->end($data, $timeout);
        } finally {
            $this->readable->close();
        }

        return $written;
    }

    /**
     * @coroutine
     *
     * Returns a coroutine that is fulfilled when the stream is ready to receive data (output buffer is not full).
     *
     * @param float|int $timeout Number of seconds until the returned promise is rejected with a TimeoutException
     *     if the data cannot be written to the stream. Use null for no timeout.
     *
     * @return \Generator
     *
     * @resolve int Always resolves with 0.
     *
     * @throws \Icicle\Stream\Exception\UnwritableException If the stream is no longer writable.
     * @throws \Icicle\Stream\Exception\ClosedException If the stream has been closed.
     */
    public function await(float $timeout = 0): \Generator
    {
        return $this->writable->await($timeout);
    }

    /**
     * {@inheritdoc}
     */
    public function isWritable(): bool
    {
        return $this->writable->isWritable();
    }

    /**
     * {@inheritdoc}
     *
     * @param float|int $timeout Timeout for poll and await if a read or write was pending.
     */
    public function rebind(float $timeout = 0)
    {
        $this->readable->rebind($timeout);
        $this->writable->rebind($timeout);
    }
}


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

/**
 * User: Evren Yurtesen
 * Date: 05-Jul-16
 * Time: 3:26 PM
 */

namespace Yurtesen\Geonames\Console;

use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\ServiceProvider;

class Install extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'geonames:install {--force : Overwrite any existing files.}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Publish the migrations and config';

    /**
     * The filesystem instance.
     *
     * @var \Illuminate\Filesystem\Filesystem
     */
    protected $files;

    /**
     * Create a new Constructor instance.
     *
     * @param Filesystem $files
     */
    public function __construct(Filesystem $files)
    {
        parent::__construct();
        $this->files = $files;
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $paths = ServiceProvider::pathsToPublish(
            'Yurtesen\Geonames\GeonamesServiceProvider'
        );

        foreach ($paths as $from => $to) {
            if ($this->files->isFile($from)) {
                $this->publishFile($from, $to);
            } elseif ($this->files->isDirectory($from)) {
                $this->publishDirectory($from, $to);
            }
        }

        $this->info("Installation complete!");
    }

    /**
     * Publish the file to the given path.
     *
     * @param  string $from
     * @param  string $to
     * @return void
     */
    protected function publishFile($from, $to)
    {
        if (!$this->files->exists($to) || $this->option('force')) {
            $this->createParentDirectory(dirname($to));

            $this->files->copy($from, $to);

            $this->status($from, $to, 'File');
        }
    }

    /**
     * Publish the directory to the given directory.
     *
     * @param  string $from
     * @param  string $to
     * @return void
     */
    protected function publishDirectory($from, $to)
    {
        $toContents = $this->files->files($to);
        $fromContents = $this->files->files($from);

        foreach ($fromContents as $file) {
            $newFile = $to . DIRECTORY_SEPARATOR . $this->files->name($file) . '.' . $this->files->extension($file);
            if ($this->files->isFile($file) && (!in_array($newFile, $toContents) || $this->option('force'))) {
                $this->files->copy($file, $newFile);
            }
        }

        $this->status($from, $to, 'Directory');
    }

    /**
     * Create the directory to house the published files if needed.
     *
     * @param  string $directory
     * @return void
     */
    protected function createParentDirectory($directory)
    {
        if (!$this->files->isDirectory($directory)) {
            $this->files->makeDirectory($directory, 0755, true);
        }
    }

    /**
     * Write a status message to the console.
     *
     * @param  string $from
     * @param  string $to
     * @param  string $type
     * @return void
     */
    protected function status($from, $to, $type)
    {
        $from = str_replace(base_path(), '', realpath($from));

        $to = str_replace(base_path(), '', realpath($to));

        $this->line('<info>Copied ' . $type . '</info> <comment>[' . $from . ']</comment> <info>To</info> <comment>[' . $to . ']</comment>');
    }
}