import functools
from typing import Optional, Dict, Union, List, Tuple

from ..utils import assert_hasattr
from ..http import HTTPUserClient, Route
from ..errors import SpotifyException
from . import (
    SpotifyBase, URIBase,
    Image, Device, Context,
    Player,
    Track,
    Artist,
    Library
)

Playlist: Optional[SpotifyBase] = None

REFRESH_TOKEN_URL = 'https://accounts.spotify.com/api/token?grant_type=refresh_token&refresh_token={refresh_token}'

def ensure_http(func):
    func.__ensure_http__ = True
    return func

class User(URIBase):
    """A Spotify User.

    Attributes
    ----------
    id : str
        The Spotify user ID for the user.
    uri : str
        The Spotify URI for the user.
    url : str
        The open.spotify URL.
    href : str
        A link to the Web API endpoint for this user.
    display_name : str
        The name displayed on the user’s profile.
        `None` if not available.
    followers : int
        The total number of followers.
    images : List[Image]
        The user’s profile image.
    email : str
        The user’s email address, as entered by the user when creating their account.
    country : str
        The country of the user, as set in the user’s account profile. An ISO 3166-1 alpha-2 country code.
    birthdate : str
        The user’s date-of-birth.
    product : str
        The user’s Spotify subscription level: “premium”, “free”, etc.
        (The subscription level “open” can be considered the same as “free”.)
    """
    def __init__(self, client, data, **kwargs):
        self.__client = client

        token = kwargs.pop('token', None)

        try:
            self.http = http = kwargs.pop('http')
        except KeyError:
            pass
        else:
            self.library = Library(client, self)

        # Public user object attributes
        self.id = data.pop('id')
        self.uri = data.pop('uri')
        self.url = data.pop('external_urls').get('spotify', None)
        self.display_name = data.pop('display_name', None)
        self.href = data.pop('href')
        self.followers = data.pop('followers', {}).get('total', None)
        self.images = list(Image(**image) for image in data.pop('images'))

        # Private user object attributes
        self.email = data.pop('email', None)
        self.country = data.pop('country', None)
        self.birthdate = data.pop('birthdate', None)
        self.product = data.pop('product', None)

    def __repr__(self):
        return '<spotify.User: "%s">' % (self.display_name or self.id)

    def __getattr__(self, key, value):
        value = object.__getattr__(self, key, value)

        if hasattr(value, '__ensure_http__') and not hasattr(self, 'http'):
            @functoosl.wraps(value)
            def _raise(*args, **kwargs):
                raise AttributeError('User has not HTTP presence to perform API requests.')
            return _raise
        else:
            return value

    async def _get_top(self, klass, data) -> List[Union[Track, Artist]]:
        _str = {Artist: 'artists', Track: 'tracks'}[klass]
        data = {key: value for key, value in data.items() if key in ('limit', 'offset', 'time_range')}

        resp = await self.http.top_artists_or_tracks(_str, **data)

        return [klass(self.__client, item) for item in resp['items']]

    async def _refreshing_token(self, expires, token):
        while True:
            await asyncio.sleep(expires - 1)

            route = ('POST', REFRESH_TOKEN_URL.format(refresh_token=token))
            data = await self.client.http.request(route, content_type='application/x-www-form-urlencoded')

            expires = data['expires_in']
            self.http.token = data['access_token']

    ### Alternate constructors

    @classmethod
    async def from_code(cls, client, code, *, redirect_uri, refresh=False):
        route = ('POST', 'https://accounts.spotify.com/api/token')
        payload = {
            'redirect_uri': redirect_uri,
            'grant_type': 'authorization_code',
            'code': code
        }

        raw = await client.http.request(route, content_type='application/x-www-form-urlencoded')

        token = raw['access_token']

        if refresh:
            refresh = (raw['expires_in'], raw['refresh_token'])
        else:
            refresh = None

        return await cls.from_token(client, token, refresh=refresh)

    @classmethod
    async def from_token(cls, client, token, *, refresh=None):
        http = HTTPUserClient(token)
        data = await http.current_user()

        if refresh is not None:
            expires_in, refresh_token, = refresh
            self._refresh_task = self.client.loop.create_task(self._refreshing_token(expires_in, refresh_token))

        return cls(client, data=data, http=http, token=token)

    ### Attributes

    @property
    def refresh(self):
        return self._refresh_task

    ### Contextual methods

    @ensure_http
    async def currently_playing(self) -> Tuple[Context, Track]:
        """Get the users currently playing track.

        Returns
        -------
        context, track : Tuple[Context, Track]
            A tuple of the context and track.
        """
        data = await self.http.currently_playing()

        if data.get('item'):
            data['Context'] = Context(data.get('context'))
            data['item'] = Track(self.__client, data.get('item'))

        return data

    @ensure_http
    async def get_player(self) -> Player:
        """Get information about the users current playback.

        Returns
        -------
        player : Player
            A player object representing the current playback.
        """
        self._player = player = Player(self.__client, self, await self.http.current_player())
        return player

    @ensure_http
    async def get_devices(self) -> List[Device]:
        """Get information about the users avaliable devices.

        Returns
        -------
        devices : List[Device]
            The devices the user has available.
        """
        data = await self.http.available_devices()
        return [Device(item) for item in data['devices']]

    @ensure_http
    async def recently_played(self) -> List[Dict[str, Union[Track, Context, str]]]:
        """Get tracks from the current users recently played tracks.

        Returns
        -------
        playlist_history : List[Dict[str, Union[Track, Context, str]]]
            A list of playlist history object.
            Each object is a dict with a timestamp, track and context field.
        """
        data = await self.http.recently_played()
        f = lambda data: {'context': Context(data.get('context')), 'track': Track(self.__client, data.get('track'))}
        # List[T] where T: {'track': Track, 'content': Context: 'timestamp': ISO8601}
        return [{'timestamp': track['timestamp'], **f(track)} for track in data['items']]

    ### Playlist track methods

    async def add_tracks(self, playlist: Union[str, Playlist], *tracks) -> str:
        """Add one or more tracks to a user’s playlist.

        Parameters
        ----------
        playlist : Union[str, Playlist]
            The playlist to modify
        tracks : Sequence[Union[str, Track]]
            Tracks to add to the playlistv

        Returns
        -------
        snapshot_id : str
            The snapshot id of the playlist.
        """
        tracks = [str(track) for track in tracks]
        data = await self.http.add_playlist_tracks(self.id, str(playlist), tracks=','.join(tracks))
        return data['snapshot_id']

    async def replace_tracks(self, playlist, *tracks) -> str:
        """Replace all the tracks in a playlist, overwriting its existing tracks.
        This powerful request can be useful for replacing tracks, re-ordering existing tracks, or clearing the playlist.

        Parameters
        ----------
        playlist : Union[str, PLaylist]
            The playlist to modify
        tracks : Sequence[Union[str, Track]]
            Tracks to place in the playlist
        """
        tracks = [str(track) for track in tracks]
        await self.http.replace_playlist_tracks(self.id, str(playlist), tracks=','.join(tracks))

    async def remove_tracks(self, playlist, *tracks):
        """Remove one or more tracks from a user’s playlist.

        Parameters
        ----------
        playlist : Union[str, Playlist]
            The playlist to modify
        tracks : Sequence[Union[str, Track]]
            Tracks to remove from the playlist

        Returns
        -------
        snapshot_id : str
            The snapshot id of the playlist.
        """
        tracks = [str(track) for track in tracks]
        data = await self.http.remove_playlist_tracks(self.id, str(playlist), tracks=','.join(tracks))
        return data['snapshot_id']

    async def reorder_tracks(self, playlist, start, insert_before, length=1, *, snapshot_id=None):
        """Reorder a track or a group of tracks in a playlist.

        Parameters
        ----------
        playlist : Union[str, Playlist]
            The playlist to modify
        start : int
            The position of the first track to be reordered.
        insert_before : int
            The position where the tracks should be inserted.
        length : Optional[int]
            The amount of tracks to be reordered. Defaults to 1 if not set.
        snapshot_id : str
            The playlist’s snapshot ID against which you want to make the changes.

        Returns
        -------
        snapshot_id : str
            The snapshot id of the playlist.
        """
        data = await self.http.reorder_playlists_tracks(self.id, str(playlist), start, length, insert_before, snapshot_id=snapshot_id)
        return data['snapshot_id']

    ### Playlist methods

    @ensure_http
    async def edit_playlist(self, playlist, *, name=None, public=None, collaborative=None, description=None):
        """Change a playlist’s name and public/private, collaborative state and description.

        Parameters
        ----------
        playlist : Union[str, Playlist]
            The playlist to modify
        name : Optional[str]
            The new name of the playlist.
        public : Optional[bool]
            The public/private status of the playlist.
            `True` for public, `False` for private.
        collaborative : Optional[bool]
            If `True`, the playlist will become collaborative and other users will be able to modify the playlist.
        description : Optional[str]
            The new playlist description
        """
        data = {}

        if name:
            data['name'] = name

        if public:
            data['public'] = public

        if collaborative:
            data['collaborative'] = collaborative

        if description:
            data['description'] = description

        await self.http.change_playlist_details(self.id, str(playlist), data)

    @ensure_http
    async def create_playlist(self, name, *, public=True, collaborative=False, description=None):
        """Create a playlist for a Spotify user.

        Parameters
        ----------
        name : str
            The name of the playlist.
        public : Optional[bool]
            The public/private status of the playlist.
            `True` for public, `False` for private.
        collaborative : Optional[bool]
            If `True`, the playlist will become collaborative and other users will be able to modify the playlist.
        description : Optional[str]
            The playlist description

        Returns
        -------
        playlist : Playlist
            The playlist that was created.
        """
        data = {
            'name': name,
            'public': public,
            'collaborative': collaborative
        }

        if description:
            data['description'] = description

        playlist_data = await self.http.create_playlist(self.id, data)
        return Playlist(self.__client, playlist_data)

    async def get_playlists(self, *, limit=20, offset=0):
        """get the users playlists from spotify.

        Parameters
        ----------
         limit : Optional[int]
             The limit on how many playlists to retrieve for this user (default is 20).
         offset : Optional[int]
             The offset from where the api should start from in the playlists.

        Returns
        -------
        playlists : List[Playlist]
            A list of the users playlists.
        """
        if hasattr(self, 'http'):
            http = self.http
        else:
            http = self.__client.http

        data = await http.get_playlists(self.id, limit=limit, offset=offset)
        return [Playlist(self.__client, playlist_data) for playlist_data in data['items']]

    async def top_artists(self, **data) -> List[Artist]:
        """Get the current user’s top artists based on calculated affinity.

        Parameters
        ----------
        limit : Optional[int]
            The number of entities to return. Default: 20. Minimum: 1. Maximum: 50.
        offset : Optional[int]
            The index of the first entity to return. Default: 0
        time_range : Optional[str]
            Over what time frame the affinities are computed. (long_term, short_term, medium_term)

        Returns
        -------
        tracks : List[Artist]
            The top artists for the user.
        """
        return await self._get_top(Artist, data)

    async def top_tracks(self, **data) -> List[Track]:
        """Get the current user’s top tracks based on calculated affinity.

        Parameters
        ----------
        limit : Optional[int]
            The number of entities to return. Default: 20. Minimum: 1. Maximum: 50.
        offset : Optional[int]
            The index of the first entity to return. Default: 0
        time_range : Optional[str]
            Over what time frame the affinities are computed. (long_term, short_term, medium_term)

        Returns
        -------
        tracks : List[Track]
            The top tracks for the user.
        """
        return await self._get_top(Track, data)


from typing import Optional, List

from . import URIBase, Image, Artist, Track

class Album(URIBase):
    """A Spotify Album.

    Attributes
    ----------
    artists : List[Artist]
        The artists for the album.
    id : str
        The ID of the album.
    name : str
        The name of the album.
    href : str
        The HTTP API URL for the album.
    uri : str
        The URI for the album.
    album_group : str
        ossible values are “album”, “single”, “compilation”, “appears_on”.
        Compare to album_type this field represents relationship between the artist and the album.
    album_type : str
        The type of the album: one of "album" , "single" , or "compilation".
    release_date : str
        The date the album was first released.
    release_date_precision : str
        The precision with which release_date value is known: year, month or day.
    genres : List[str]
        A list of the genres used to classify the album.
    label : str
        The label for the album.
    popularity : int
        The popularity of the album. The value will be between 0 and 100, with 100 being the most popular.
    copyrights : List[Dict]
        The copyright statements of the album.
    markets : List[str]
        The markets in which the album is available: ISO 3166-1 alpha-2 country codes.
    """
    def __init__(self, client, data):
        self.__client = client

        # Simple object attributes.
        self.type = data.pop('album_type', None)
        self.group = data.pop('album_group', None)
        self.artists = [Artist(client, artist) for artist in data.pop('artists', [])]

        if self.artists:
            self.artist = self.artists[0]

        self.markets = data.pop('avaliable_markets', None)
        self.url = data.pop('external_urls').get('spotify', None)
        self.id = data.pop('id', None)
        self.name = data.pop('name', None)
        self.href = data.pop('href', None)
        self.uri = data.pop('uri', None)
        self.release_date = data.pop('release_date', None)
        self.release_date_precision = data.pop('release_date_precision', None)
        self.images = list(Image(**image) for image in data.pop('images', []))
        self.restrictions = data.pop('restrictions', None)

        # Full object attributes
        self.genres = data.pop('genres', None)
        self.copyrights = data.pop('copyrights', None)
        self.label = data.pop('label', None)
        self.popularity = data.pop('popularity', None)
        self.total_tracks = data.pop('total_tracks', None)

    def __repr__(self):
        return '<spotify.Album: "%s">' % (self.name or self.id or self.uri)

    async def get_tracks(self, *, limit: Optional[int] = 20, offset: Optional[int] = 0) -> List[Track]:
        """get the albums tracks from spotify.

        Parameters
        ----------
        limit : Optional[int]
            The limit on how many tracks to retrieve for this album (default is 20).
        offset : Optional[int]
            The offset from where the api should start from in the tracks.

        Returns
        -------
        tracks : List[Track]
            The tracks of the artist.
        """
        data = await self.__client.http.album_tracks(self.id, limit=limit, offset=offset)
        return list(Track(self.__client, item) for item in data['items'])

    async def get_all_tracks(self, *, market: Optional[str] = 'US') -> List[Track]:
        """loads all of the albums tracks, depending on how many the album has this may be a long operation.

        Parameters
        ----------
        market : Optional[str]
            An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking.

        Returns
        -------
        tracks : List[Track]
            The tracks of the artist.
        """
        tracks = []
        offset = 0
        total = self.total_tracks or None

        while True:
            data = await self.__client.http.album_tracks(self.id, limit=50, offset=offset, market=market)

            if total is None:
                total = data['total']

            offset += 50
            tracks += list(Track(self.__client, item) for item in data['items'])

            if len(tracks) >= total:
                break

        return tracks


import asyncio
from typing import Optional, List, Iterable, Dict, Union

from .http import HTTPClient
from .utils import to_id
from . import (
    OAuth2,
    Artist,
    Album,
    Track,
    User,
    Playlist,
    Library,
    PlaylistTrack
)

_TYPES = {
    'artist': Artist,
    'album': Album,
    'playlist': Playlist,
    'track': Track
}

_SEARCH_TYPES = {'track', 'playlist', 'artist', 'album'}
_SEARCH_TYPE_ERR = 'Bad queary type! got "%s" expected any of: track, playlist, artist, album'

class Client:
    """Represents a Client app on Spotify.

    This class is used to interact with the Spotify API.

    Parameters
    ----------
    client_id : str
        The client id provided by spotify for the app.
    client_secret : str
        The client secret for the app.
    loop : asyncio.AbstractEventLoop
        The event loop the client should run on, if no loop is specified `asyncio.get_event_loop()` is called and used instead.

    Attributes
    ----------
    client_id : str
        The applications client_id, also aliased as `id`
    http : HTTPClient
        The HTTPClient that is being used.
    loop : asyncio.AbstractEventLoop
        The event loop the client is running on.
    """
    _default_http_client = HTTPClient

    def __init__(self, client_id, client_secret, *, loop=None):
        self.loop = loop or asyncio.get_event_loop()
        self.http = self._default_http_client(client_id, client_secret, loop=loop)

    def __repr__(self):
        return '<spotify.Client: "%s">' % self.http.client_id

    @property
    def client_id(self):
        return self.http.client_id

    @property
    def id(self):
        return self.http.client_id

    def oauth2_url(self, redirect_uri: str, scope: Optional[str] = None, state: Optional[str] = None) -> str:
        """Generate an outh2 url for user authentication.

        Parameters
        ----------
        redirect_uri : str
            Where spotify should redirect the user to after authentication.
        scope : Optional[str]
            Space seperated spotify scopes for different levels of access.
        state : Optional[str]
            Using a state value can increase your assurance that an incoming connection is the result of an authentication request.

        Returns
        -------
        url : str
            The OAuth2 url.
        """
        return OAuth2.url_(self.http.client_id, redirect_uri, scope=scope, state=state)

    async def close(self):
        """Close the underlying HTTP session to Spotify."""
        await self.http.close()

    async def user_from_token(self, token: str) -> User:
        """Create a user session from a token.

        This code is equivelent to `User.from_token(client, token)`

        Parameters
        ----------
        token : str
            The token to attatch the user session to.

        Returns
        -------
        user : User
            The user from the ID
        """
        return await User.from_token(self, token)

    async def get_album(self, spotify_id: str, *, market: str = 'US') -> Album:
        """Retrive an album with a spotify ID.

        Parameters
        ----------
        spotify_id : str
            The ID to search for.
        market : Optional[str]
            An ISO 3166-1 alpha-2 country code

        Returns
        -------
        album : Album
            The album from the ID
        """
        data = await self.http.album(to_id(spotify_id), market=market)
        return Album(self, data)

    async def get_artist(self, spotify_id: str) -> Artist:
        """Retrive an artist with a spotify ID.

        Parameters
        ----------
        spotify_id : str
            The ID to search for.

        Returns
        -------
        artist : Artist
            The artist from the ID
        """
        data = await self.http.artist(to_id(spotify_id))
        return Artist(self, data)

    async def get_track(self, spotify_id: str) -> Track:
        """Retrive an track with a spotify ID.

        Parameters
        ----------
        spotify_id : str
            The ID to search for.

        Returns
        -------
        track : Track
            The track from the ID
        """
        data = await self.http.track(to_id(spotify_id))
        return Track(self, data)

    async def get_user(self, spotify_id: str) -> User:
        """Retrive an user with a spotify ID.

        Parameters
        ----------
        spotify_id : str
            The ID to search for.

        Returns
        -------
        user : User
            The user from the ID
        """
        data = await self.http.user(to_id(spotify_id))
        return User(self, data)

    # Get multiple objects

    async def get_albums(self, *ids: List[str], market: str = 'US') -> List[Album]:
        """Retrive multiple albums with a list of spotify IDs.

        Parameters
        ----------
        ids : List[str]
            the ID to look for
        market : Optional[str]
            An ISO 3166-1 alpha-2 country code

        Returns
        -------
        albums : List[Album]
            The albums from the IDs
        """
        data = await self.http.albums(','.join(to_id(_id) for _id in ids), market=market)
        return list(Album(self, album) for album in data['albums'])

    async def get_artists(self, *ids: List[str]) -> List[Artist]:
        """Retrive multiple artists with a list of spotify IDs.

        Parameters
        ----------
        ids : List[str]
            the IDs to look for

        Returns
        -------
        artists : List[Artist]
            The artists from the IDs
        """
        data = await self.http.artists(','.join(to_id(_id) for _id in ids))
        return list(Artist(self, artist) for artist in data['artists'])

    async def search(self, q: str, *, types: Optional[Iterable[str]] = ['track', 'playlist', 'artist', 'album'], limit: Optional[int] = 20, offset: Optional[int] = 0, market: Optional[str] = None) -> Dict[str, List[Union[Track, Playlist, Artist, Album]]]:
        """Access the spotify search functionality.

        Parameters
        ----------
        q : str
            the search query
        types : Optional[Iterable[str]]
            A sequence of search types (can be any of `track`, `playlist`, `artist` or `album`) to refine the search request.
            A `ValueError` may be raised if a search type is found that is not valid.
        limit : Optional[int]
            The limit of search results to return when searching.
            Maximum limit is 50, any larger may raise a :class:`HTTPException`
        offset : Optional[int]
            The offset from where the api should start from in the search results.
        market : Optional[str]
            An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking.

        Returns
        -------
        results : Dict[str, List[Union[Track, Playlist, Artist, Album]]]
            The results of the search.
        """
        if not hasattr(types, '__iter__'):
            raise TypeError('types must be an iterable.')

        elif not isinstance(types, list):
            types = list(item for item in types)

        types_ = set(types)

        if not types_.issubset(_SEARCH_TYPES):
            raise ValueError(_SEARCH_TYPE_ERR % types_.difference(_SEARCH_TYPES).pop())

        kwargs = {
            'q': q.replace(' ', '+'),
            'queary_type': ','.join(tp.strip() for tp in types),
            'market': market,
            'limit': limit,
            'offset': offset
        }

        data = await self.http.search(**kwargs)

        return {key: [_TYPES[obj['type']](self, obj) for obj in value['items']] for key, value in data.items()}


from typing import Sequence, Union, List

from . import SpotifyBase
from .track import Track
from .album import Album

class Library(SpotifyBase):
    """A Spotify Users Library.

    Attributes
    ----------
    user : Spotify.User
        The user which this library object belongs to.
    """
    def __init__(self, client, user):
        self.user = user
        self.__client = client

    def __repr__(self):
        return '<spotify.Library: %s>' % (repr(self.user))

    def __eq__(self, other):
        return type(self) is type(other) and self.user == other.user

    def __ne__(self, other):
        return not self.__eq__(other)

    async def contains_albums(self, *albums: Sequence[Union[str, Album]]) -> List[bool]:
        """Check if one or more albums is already saved in the current Spotify user’s ‘Your Music’ library.

        Parameters
        ----------
        albums : Union[Album, str]
            A sequence of artist objects or spotify IDs
        """
        _albums = [(obj if isinstance(obj, str) else obj.id) for obj in albums]
        return await self.user.http.is_saved_album(_albums)

    async def contains_tracks(self, *tracks: Sequence[Union[str, Track]]) -> List[bool]:
        """Check if one or more tracks is already saved in the current Spotify user’s ‘Your Music’ library.

        Parameters
        ----------
        tracks : Union[Track, str]
            A sequence of track objects or spotify IDs
        """
        _tracks = [(obj if isinstance(obj, str) else obj.id) for obj in tracks]
        return await self.user.http.is_saved_track(_tracks)

    async def get_tracks(self, *, limit=20, offset=0) -> List[Track]:
        """Get a list of the songs saved in the current Spotify user’s ‘Your Music’ library.

        Parameters
        ----------
        limit : Optional[int]
            The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50.
        offset : Optional[int]
            The index of the first item to return. Default: 0
        """
        data = await self.user.http.saved_tracks(limit=limit, offset=offset)

        return [Track(self.__client, item['track']) for item in data['items']]

    async def get_albums(self, *, limit=20, offset=0) -> List[Album]:
        """Get a list of the albums saved in the current Spotify user’s ‘Your Music’ library.

        Parameters
        ----------
        limit : Optional[int]
            The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50.
        offset : Optional[int]
            The index of the first item to return. Default: 0
        """
        data = await self.user.http.saved_albums(limit=limit, offset=offset)

        return [Album(self.__client, item['album']) for item in data['items']]

    async def remove_albums(self, *albums):
        """Remove one or more albums from the current user’s ‘Your Music’ library.

        Parameters
        ----------
        albums : Sequence[Union[Album, str]]
            A sequence of artist objects or spotify IDs
        """
        _albums = [(obj if isinstance(obj, str) else obj.id) for obj in albums]
        await self.user.http.delete_saved_albums(','.join(_albums))

    async def remove_tracks(self, *tracks):
        """Remove one or more tracks from the current user’s ‘Your Music’ library.

        Parameters
        ----------
        tracks : Sequence[Union[Track, str]]
            A sequence of track objects or spotify IDs
        """
        _tracks = [(obj if isinstance(obj, str) else obj.id) for obj in tracks]
        await self.user.http.delete_saved_tracks(','.join(_tracks))

    async def save_albums(self, *albums):
        """Save one or more albums to the current user’s ‘Your Music’ library.

        Parameters
        ----------
        albums : Sequence[Union[Album, str]]
            A sequence of artist objects or spotify IDs
        """
        _albums = [(obj if isinstance(obj, str) else obj.id) for obj in albums]
        await self.user.http.save_albums(','.join(_albums))

    async def save_tracks(self, *tracks):
        """Save one or more tracks to the current user’s ‘Your Music’ library.

        Parameters
        ----------
        tracks : Sequence[Union[Track, str]]
            A sequence of track objects or spotify IDs
        """
        _tracks = [(obj if isinstance(obj, str) else obj.id) for obj in tracks]
        await self.user.http.save_tracks(','.join(_tracks))


import inspect
import functools
import re
from contextlib import contextmanager
from urllib.parse import quote_plus as quote
from typing import Callable

from .errors import SpotifyException

_URI_RE = re.compile(r'^.*:([a-zA-Z0-9]+)$')
_OPEN_RE = re.compile(r'http[s]?:\/\/open\.spotify\.com\/(.*)\/(.*)')

@contextmanager
def clean(l: dict, *names):
    yield
    for name in names:
        l.pop(name)

def to_id(string: str) -> str:
    """Get a spotify ID from a URI or open.spotify URL.

    Paramters
    ---------
    string : str
        The string to operate on.

    Returns
    -------
    id : str
        The Spotify ID from the string.
    """
    string = string.strip()

    match = _URI_RE.match(string)

    if match is None:
        match = _OPEN_RE.match(string)

        if match is None:
            return string
        else:
            return match.group(2)
    else:
        return match.group(1)

def assert_hasattr(attr: str, msg: str, tp: BaseException = SpotifyException) -> Callable:
    """decorator to assert an object has an attribute when run."""
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def decorated(self, *args, **kwargs):
            if not hasattr(self, attr):
                raise tp(msg)
            return func(self, *args, **kwargs)

        if inspect.iscoroutinefunction(func):
            @functools.wraps(func)
            async def decorated(*args, **kwargs):
                return await decorated(*args, **kwargs)

        return decorated
    return decorator

class OAuth2:
    """Helper object for Spotify OAuth2 operations.

    Parameters
    ----------
    client_id : str
        The client id provided by spotify for the app.
    redirect_uri : str
        The URI Spotify should redirect to.
    scope : str
        The scopes to be requested.
    state : str
        The state used to secure sessions.

    Attributes
    ----------
    attrs : Dict[str, str]
        The attributes used when constructing url parameters
    parameters : str
        The URL parameters used
    url : str
        The URL for OAuth2
    """
    _BASE = 'https://accounts.spotify.com/authorize/?response_type=code&{parameters}'

    def __init__(self, client_id, redirect_uri, *, scope=None, state=None, secure=True):
        self.client_id = client_id
        self.redirect_uri = redirect_uri

        self.state = state
        self.scope = scope

    def __repr__(self):
        return '<spotfy.OAuth2: client_id={0!r}, scope={1!r}>'.format(self.client_id, self.scope)

    def __str__(self):
        return self.url

    @classmethod
    def from_client(cls, client, *args, **kwargs):
        """Construct a OAuth2 object from a `spotify.Client`."""
        return cls(client.http.client_id, *args, **kwargs)

    @staticmethod
    def url_(client_id: str, redirect_uri: str, *, scope: str = None, state: str = None, secure: bool = True) -> str:
        """Construct a OAuth2 URL instead of an OAuth2 object."""
        attrs = {
            'client_id': client_id,
            'redirect_uri': quote(redirect_uri)
        }

        if scope is not None:
            attrs['scope'] = quote(scope)

        if state is not None:
            attrs['state'] = state

        parameters = '&'.join('{0}={1}'.format(*item) for item in attrs.items())

        return OAuth2._BASE.format(parameters=parameters)

    @staticmethod
    def url_only(*args, **kwargs):
        """Construct a OAuth2 URL instead of an OAuth2 object."""
        return OAuth2.url_(*args, **kwargs)

    @property
    def attrs(self):
        """Attributes used when constructing url parameters."""
        data = {
            'client_id': self.client_id,
            'redirect_uri': quote(self.redirect_uri),
        }

        if self.scope is not None:
            data['scope'] = quote(self.scope)

        if self.state is not None:
            data['state'] = self.state

        return data

    @property
    def parameters(self) -> str:
        """URL parameters used."""
        return '&'.join('{0}={1}'.format(*item) for item in self.attrs.items())

    @property
    def url(self) -> str:
        """Return a URL for an OAuth2."""
        return self._BASE.format(parameters=self.parameters)


from typing import List
from functools import partial

from . import SpotifyBase, URIBase, Track, PlaylistTrack, Image

class PartialTracks(SpotifyBase):
    """A partial track object which contains information about the tracks but not the tracks itself.

    Attributes
    ----------
    total : int
        The total amount of tracks.
    """
    __slots__ = ('total', '__func', '__iter', '__client')

    def __init__(self, client, data):
        self.total = data['total']
        self.__client = client
        self.__func = partial(client.http.request, ('GET', data['href']))
        self.__iter = None

    def __repr__(self):
        return '<spotify.PartialTracks: total="%s">' % self.total

    def __aiter__(self):
        return self

    async def __anext__(self):
        if self.__iter is None:
            self.__iter = iter((await self.__func())['items'])

        try:
            track = next(self.__iter)
        except StopIteration:
            raise StopAsyncIteration
        else:
            return PlaylistTrack(self.__client, track)

    async def build(self):
        """get the track object for each link in the partial tracks data

        Returns
        -------
        tracks : List[Track]
            The tracks
        """
        data = await self.__func()
        return list(PlaylistTrack(self.__client, track) for track in data['items'])

class Playlist(URIBase):
    """A Spotify Playlist.

    Attributes
    ----------
    collaborative : bool
        Returns true if context is not search and the owner allows other users to modify the playlist. Otherwise returns false.
    description : str
        The playlist description. Only returned for modified, verified playlists, otherwise null.
    url : str
        The open.spotify URL.
    followers : int
        The total amount of followers
    href : str
        A link to the Web API endpoint providing full details of the playlist.
    id : str
        The Spotify ID for the playlist.
    images : List[Image]
        Images for the playlist.
        The array may be empty or contain up to three images.
        The images are returned by size in descending order.
        If returned, the source URL for the image ( url ) is temporary and will expire in less than a day.
    name : str
        The name of the playlist.
    owner : User
        The user who owns the playlist
    public : bool
        The playlist’s public/private status:
            true the playlist is public,
            false the playlist is private,
            null the playlist status is not relevant.
    snapshot_id : str
        The version identifier for the current playlist.
    tracks : Option[List[PlaylistTrack]]
        list of playlist track objects.
    """

    def __init__(self, client, data):
        from .user import User

        self.__client = client

        self.collaborative = data.pop('collaborative')
        self.description = data.pop('description')
        self.url = data.pop('external_urls').get('spotify', None)
        self.followers = data.pop('followers', {}).get('total', None)
        self.href = data.pop('href')
        self.id = data.pop('id')
        self.images = list(Image(**image) for image in data.pop('images', []))
        self.name = data.pop('name')
        self.owner = User(client, data=data.pop('owner'))
        self.uri = data.pop('uri')

        if 'next' in data['tracks']:  # Paging object.
            pass  # TODO: Support paging objects.
        else:
            self._tracks = tracks = PartialTracks(client, data.pop('tracks'))
            self.total_tracks = _tracks.total

    def __repr__(self):
        return '<spotify.Playlist: "%s">' % self.name

    async def get_all_tracks(self) -> List[PlaylistTrack]:
        """Get all playlist tracks from the playlist.

        Returns
        -------
        tracks : List[PlaylistTrack]
            The playlists tracks.
        """
        if isinstance(self._tracks, PartialTracks):
            return await self._tracks.build()

        _tracks = []
        offset = 0
        while len(self.tracks) < self.total_tracks:
            data = await self.__client.http.get_playlist_tracks(self.owner.id, self.id, limit=50, offset=offset)

            _tracks += [PlaylistTrack(self.__client, item) for item in data['items']]
            offset += 50

        self.total_tracks = len(self._tracks)
        return list(self._tracks)


from typing import Union, Optional, Sequence

from .. import HTTPException
from . import SpotifyBase, URIBase, Context, Device, Track
from .typing import SomeURI, SomeURIs

Offset = Union[int, str, Track]
SomeDevice = Union[Device, str]

class Player(SpotifyBase):
    """A Spotify Users current playback.

    Attributes
    ----------
    device : Device
        The device that is currently active.
    repeat_state : str
        "off", "track", "context"
    shuffle_state : bool
        If shuffle is on or off.
    context : spotify.Context
        The context of the current player.
    timestamp : int
        Unix Millisecond Timestamp when data was fetched.
    progress_ms : int
        Progress into the currently playing track.
        Can be None (e.g. If private session is enabled this will be None).
    is_playing : bool
        If something is currently playing.
    item : Track
        The currently playing track.
        Can be None (e.g. If private session is enabled this will be None).
    currently_playing_type : str
        The object type of the currently playing item. #
        Can be one of "track", "episode", "ad" or "unknown".
    actions : List[str]
        A list of disallowed actions.
    """

    def __init__(self, client, user, data):
        self.__client = client
        self.__user = user

        self.repeat_state = data.get('repeat_state', None)
        self.timestamp = data.pop('timestamp', None)
        self.progress_ms = data.pop('progress_ms', None)
        self.shuffle_state = data.pop('shuffle_state', None)
        self.is_playing = data.pop('is_playing', None)

        context = data.pop('context', None)
        if context:
            self.context = Context(context)

        device = data.pop('device', None)
        if device:
            self.device = Device(device)

        item = data.pop('item', None)
        if item:
            self.item = Track(client, item)

    def __repr__(self):
        return '<spotify.Player: "%s">' % repr(self._user)

    # Properties

    @property
    def user(self):
        return self.__user

    # Public methods

    async def pause(self, *, device: Optional[SomeDevice] = None):
        """Pause playback on the user’s account.

        Parameters
        ----------
        device : Optional[:obj:`SomeDevice`]
            The Device object or id of the device this command is targeting.
            If not supplied, the user’s currently active device is the target.
        """
        await self._user.http.pause_playback(device_id=str(device))

    async def resume(self, *, device: Optional[SomeDevice] = None):
        """Resume playback on the user's account.

        Parameters
        ----------
        device : Optional[:obj:`SomeDevice`]
            The Device object or id of the device this command is targeting.
            If not supplied, the user’s currently active device is the target.
        """
        await self._user.http.play_playback(None, device_id=str(device))

    async def seek(self, pos, *, device: Optional[SomeDevice] = None):
        """Seeks to the given position in the user’s currently playing track.

        Parameters
        ----------
        pos : int
            The position in milliseconds to seek to.
            Must be a positive number.
            Passing in a position that is greater than the length of the track will cause the player to start playing the next song.
        device : Optional[:obj:`SomeDevice`]
            The Device object or id of the device this command is targeting.
            If not supplied, the user’s currently active device is the target.
        """
        await self._user.http.seek_playback(pos, device_id=str(device))

    async def set_repeat(self, state, *, device: Optional[SomeDevice] = None):
        """Set the repeat mode for the user’s playback.

        Parameters
        ----------
        state : str
            Options are repeat-track, repeat-context, and off
        device : Optional[:obj:`SomeDevice`]
            The Device object or id of the device this command is targeting.
            If not supplied, the user’s currently active device is the target.
        """
        await self._user.http.repeat_playback(state, device_id=str(device))

    async def set_volume(self, volume: int, *, device: Optional[SomeDevice] = None):
        """Set the volume for the user’s current playback device.

        Parameters
        ----------
        volume : int
            The volume to set. Must be a value from 0 to 100 inclusive.
        device : Optional[:obj:`SomeDevice`]
            The Device object or id of the device this command is targeting.
            If not supplied, the user’s currently active device is the target.
        """
        await self._user.http.set_playback_volume(volume, device_id=str(device))

    async def next(self, *, device: Optional[SomeDevice] = None):
        """Skips to next track in the user’s queue.

        Parameters
        ----------
        device : Optional[:obj:`SomeDevice`]
            The Device object or id of the device this command is targeting.
            If not supplied, the user’s currently active device is the target.
        """
        await self._user.http.skip_next(device_id=str(device))

    async def previous(self, *, device: Optional[SomeDevice] = None):
        """Skips to previous track in the user’s queue.

        Note that this will ALWAYS skip to the previous track, regardless of the current track’s progress.
        Returning to the start of the current track should be performed using :meth:`seek`

        Parameters
        ----------
        device : Optional[:obj:`SomeDevice`]
            The Device object or id of the device this command is targeting.
            If not supplied, the user’s currently active device is the target.
        """
        return await self._user.http.skip_previous(device_id=str(device))

    async def play(self, *uris: SomeURIs, offset: Optional[Offset] = 0, device: Optional[SomeDevice] = None):
        """Start a new context or resume current playback on the user’s active device.

        The method treats a single argument as a Spotify context, such as a Artist, Album and playlist objects/URI.
        When called with multiple positional arguments they are interpreted as a array of Spotify Track objects/URIs.

        Parameters
        ----------
        *uris : :obj:`SomeURIs`
            When a single argument is passed in that argument is treated as a context.
            Valid contexts are: albums, artists, playlists.
            Album, Artist and Playlist objects are accepted too.
            Otherwise when multiple arguments are passed in they,
            A sequence of Spotify Tracks or Track URIs to play.
        offset : Optional[:obj:`Offset`]
            Indicates from where in the context playback should start.
            Only available when `context` corresponds to an album or playlist object,
            or when the `uris` parameter is used. when an integer offset is zero based and can’t be negative.
        device : Optional[:obj:`SomeDevice`]
            The Device object or id of the device this command is targeting.
            If not supplied, the user’s currently active device is the target.
        """
        if len(uris) > 1:
            # Regular uris paramter
            context_uri = list(str(uri) for uri in uris)
        else:
            # Treat it as a context URI
            context_uri = str(uris[0])

        if device is not None:
            if not isinstance(device, (Device, str)):
                raise TypeError('Expected `device` to either be a spotify.Device or a string. got {type(0)!r}'.format(device))
            else:
                device = device.id

        await self._user.http.play_playback(context_uri, offset=offset, device_id=device)

    async def shuffle(self, state: Optional[bool] = None, *, device: Optional[SomeDevice] = None):
        """shuffle on or off for user’s playback.

        Parameters
        ----------
        state : Optional[bool]
            if `True` then Shuffle user’s playback.
            else if `False` do not shuffle user’s playback.
        device : Optional[:obj:`SomeDevice`]
            The Device object or id of the device this command is targeting.
            If not supplied, the user’s currently active device is the target.
        """
        await self.__user.http.shuffle_playback(state)

    async def transfer(self, device: SomeDevice, ensure_playback: bool = False):
        """Transfer playback to a new device and determine if it should start playing.

        Parameters
        ----------
        device : :obj:`SomeDevice`
            The device on which playback should be started/transferred.
        ensure_playback : bool
            if `True` ensure playback happens on new device.
            else keep the current playback state.
        """
        await self._user.http.transfer_player(str(device), play=ensure_playback)


import spotify

class SpotifyBase:
    """The base class all Spotify models should derive from."""
    __slots__ = ()

    def __new__(cls, client, *args, **kwargs):
        if not isinstance(client, spotify.Client):
            raise TypeError(f'{cls!r}: expected client argument to be an instance of spotify.Client. instead got {type(client)}')

        elif hasattr(client, '__client_thread__'):
            cls = getattr(spotify.sync.models, cls.__name__)

        return object.__new__(cls)

    async def from_href(self):
        """Get the full object from spotify with a `href` attribute."""
        if not hasattr(self, 'href'):
            raise TypeError('Spotify object has no `href` attribute, therefore cannot be retrived')

        elif hasattr(self, 'http'):
            return await self.http.request(('GET', self.href))

        else:
            cls = type(self)

        try:
            client = getattr(self, '_{0}__client'.format(cls.__name__))
        except AttributeError:
            raise TypeError('Spotify object has no way to access a HTTPClient.')
        else:
            http = client.http

        data = await http.request(('GET', self.href))

        return cls(client, data)

class URIBase(SpotifyBase):
    """Base class used for inheriting magic methods for models who have URIs."""

    def __eq__(self, other):
        return type(self) is type(other) and self.uri == other.uri

    def __ne__(self, other):
        return not self.__eq__(other)

    def __str__(self):
        return self.uri


#!/usr/bin/env python3

# pylint:disable=line-too-long
"""
The tool to check the availability or syntax of domains, IPv4 or URL.

::

    ██████╗ ██╗   ██╗███████╗██╗   ██╗███╗   ██╗ ██████╗███████╗██████╗ ██╗     ███████╗
    ██╔══██╗╚██╗ ██╔╝██╔════╝██║   ██║████╗  ██║██╔════╝██╔════╝██╔══██╗██║     ██╔════╝
    ██████╔╝ ╚████╔╝ █████╗  ██║   ██║██╔██╗ ██║██║     █████╗  ██████╔╝██║     █████╗
    ██╔═══╝   ╚██╔╝  ██╔══╝  ██║   ██║██║╚██╗██║██║     ██╔══╝  ██╔══██╗██║     ██╔══╝
    ██║        ██║   ██║     ╚██████╔╝██║ ╚████║╚██████╗███████╗██████╔╝███████╗███████╗
    ╚═╝        ╚═╝   ╚═╝      ╚═════╝ ╚═╝  ╚═══╝ ╚═════╝╚══════╝╚═════╝ ╚══════╝╚══════╝

This submodule will provide the exipration date extraction logic.

Author:
    Nissar Chababy, @funilrys, contactTATAfunilrysTODTODcom

Special thanks:
    https://pyfunceble.readthedocs.io/en/master/special-thanks.html

Contributors:
    http://pyfunceble.readthedocs.io/en/master/special-thanks.html

Project link:
    https://github.com/funilrys/PyFunceble

Project documentation:
    https://pyfunceble.readthedocs.io/en/master/

Project homepage:
    https://funilrys.github.io/PyFunceble/

License:
::

    MIT License

    Copyright (c) 2017-2019 Nissar Chababy

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
"""
# pylint: enable=line-too-long
# pylint: disable=bad-continuation
import PyFunceble
from PyFunceble.check import Check
from PyFunceble.database import Whois
from PyFunceble.generate import Generate
from PyFunceble.helpers import Regex
from PyFunceble.http_code import HTTPCode
from PyFunceble.logs import Logs
from PyFunceble.lookup import Lookup
from PyFunceble.referer import Referer

class ExpirationDate:  # pylint: disable=too-few-public-methods
    """
    Get, format and return the expiration date of a domain, if exist.
    """

    def __init__(self):
        # We set the log separator.
        self.log_separator = "=" * 100 + " \n"

        # We initiate a variable which will save the extracted expiration date.
        self.expiration_date = ""

        # We initate a variable which will save our WHOIS record.s
        self.whois_record = ""

        # We initate an instance of Check
        self.checker = Check()

    def get(self):  # pragma: no cover
        """
        Execute the logic behind the meaning of ExpirationDate + return the matched status.

        :return:
            The status of the tested domain.
            Can be one of the official status.
        :rtype: str
        """

        # We get the status of the domain validation.
        domain_validation = self.checker.is_domain_valid()
        # We get the status of the IPv4 validation.
        ip_validation = self.checker.is_ip_valid()

        if "current_test_data" in PyFunceble.INTERN:
            # The end-user want more information whith his test.

            # We update some index.
            PyFunceble.INTERN["current_test_data"].update(
                {
                    "domain_syntax_validation": domain_validation,
                    "ip4_syntax_validation": ip_validation,
                }
            )

        if (
            domain_validation
            and not ip_validation
            or domain_validation
            or PyFunceble.CONFIGURATION["local"]
        ):
            # * The element is a valid domain.
            # and
            # * The element is not ahe valid IPv4.
            # or
            # * The element is a valid domain.

            # * We get the HTTP status code of the currently tested element.
            # and
            # * We try to get the element status from the IANA database.
            PyFunceble.INTERN.update(
                {"http_code": HTTPCode().get(), "referer": Referer().get()}
            )

            if not PyFunceble.INTERN["referer"]:
                # We could not get the referer.

                # We parse the referer status into the upstream call.
                return PyFunceble.INTERN["referer"]

            # The WHOIS record status is not into our list of official status.

            if PyFunceble.INTERN["referer"] and not self.checker.is_subdomain():
                # * The iana database comparison status is not None.
                # and
                # * The domain we are testing is not a subdomain.

                # We try to extract the expiration date from the WHOIS record.
                # And we return the matched status.
                return self._extract()

            # The iana database comparison status is None.

            # We log our whois record if the debug mode is activated.
            Logs().whois(self.whois_record)

            # And we return None, we could not extract the expiration date.
            return None

        if (
            ip_validation
            and not domain_validation
            or ip_validation
            or PyFunceble.CONFIGURATION["local"]
        ):
            # * The element is a valid IPv4.
            # and
            # * The element is not a valid domain.
            # or
            # * The element is a valid IPv4.

            # We get the HTTP status code.
            PyFunceble.INTERN["http_code"] = HTTPCode().get()

            # We log our whois record if the debug mode is activated.
            Logs().whois(self.whois_record)

            # And we return None, there is no expiration date to look for.
            return None

        # The validation was not passed.

        # We log our whois record if the debug mode is activated.
        Logs().whois(self.whois_record)

        # And we return False, the domain could not pass the IP and domains syntax validation.
        return False

    @classmethod
    def _convert_1_to_2_digits(cls, number):
        """
        Convert 1 digit number to two digits.

        :param number: A number or a digit string.
        :type number: str|int

        :return: A 2 or more digit string.
        :rtype: str
        """

        return str(number).zfill(2)

    @classmethod
    def _convert_or_shorten_month(cls, data):
        """
        Convert a given month into our unified format.

        :param data: The month to convert or shorten.
        :type data: str

        :return: The unified month name.
        :rtype: str
        """

        # We map the different month and their possible representation.
        short_month = {
            "jan": [str(1), "01", "Jan", "January"],
            "feb": [str(2), "02", "Feb", "February"],
            "mar": [str(3), "03", "Mar", "March"],
            "apr": [str(4), "04", "Apr", "April"],
            "may": [str(5), "05", "May"],
            "jun": [str(6), "06", "Jun", "June"],
            "jul": [str(7), "07", "Jul", "July"],
            "aug": [str(8), "08", "Aug", "August"],
            "sep": [str(9), "09", "Sep", "September"],
            "oct": [str(10), "Oct", "October"],
            "nov": [str(11), "Nov", "November"],
            "dec": [str(12), "Dec", "December"],
        }

        for month in short_month:
            # We loop through our map.

            if data in short_month[month]:
                # If the parsed data (or month if you prefer) is into our map.

                # We return the element (or key if you prefer) assigned to
                # the month.
                return month

        # The element is not into our map.

        # We return the parsed element (or month if you prefer).
        return data

    def _cases_management(self, regex_number, matched_result):
        """
        A little internal helper of self.format. (Avoiding of nested loops)

        .. note::
            Please note that the second value of the case represent the groups
            in order :code:`[day,month,year]`.

            This means that a :code:`[2,1,0]` will be for example for a date
            in format :code:`2017-01-02` where
            :code:`01` is the month.

        :param regex_number: The identifiant of the regex.
        :type regex_number: int

        :param matched_result: The matched result to format.
        :type matched_result: list

        :return:
            A list representing the expiration date.
            The list can be "decoded" like :code:`[day, month, year]`
        :rtype: list|None
        """

        # We map our regex numbers with with the right group order.
        # Note: please report to the method note for more information about the mapping.
        cases = {
            "first": [[1, 2, 3, 10, 11, 22, 26, 27, 28, 29, 32, 34, 38], [0, 1, 2]],
            "second": [[14, 15, 31, 33, 36, 37], [1, 0, 2]],
            "third": [
                [4, 5, 6, 7, 8, 9, 12, 13, 16, 17, 18, 19, 20, 21, 23, 24, 25, 30, 35],
                [2, 1, 0],
            ],
        }

        for case in cases:
            # We loop through the cases.

            # We get the case data.
            case_data = cases[case]

            if int(regex_number) in case_data[0]:
                # The regex number is into the currently read case data.

                # We return a list with the formatted elements.
                # 1. We convert the day to 2 digits.
                # 2. We convert the month to the unified format.
                # 3. We return the year.
                return [
                    self._convert_1_to_2_digits(matched_result[case_data[1][0]]),
                    self._convert_or_shorten_month(matched_result[case_data[1][1]]),
                    str(matched_result[case_data[1][2]]),
                ]

        # The regex number is not already mapped.

        # We return the parsed data.
        return matched_result  # pragma: no cover

    def _format(self, date_to_convert=None):
        """
        Format the expiration date into an unified format (01-jan-1970).

        :param date_to_convert:
            The date to convert. In other words, the extracted date.
        :type date_to_convert: str

        :return: The formatted expiration date.
        :rtype: str
        """

        if not date_to_convert:  # pragma: no cover
            # The date to conver is given.

            # We initiate the date we are working with.
            date_to_convert = self.expiration_date

        # We map the different possible regex.
        # The regex index represent a unique number which have to be reported
        # to the self._case_management() method.
        regex_dates = {
            # Date in format: 02-jan-2017
            "1": r"([0-9]{2})-([a-z]{3})-([0-9]{4})",
            # Date in format: 02.01.2017 // Month: jan
            "2": r"([0-9]{2})\.([0-9]{2})\.([0-9]{4})$",
            # Date in format: 02/01/2017 // Month: jan
            "3": r"([0-3][0-9])\/(0[1-9]|1[012])\/([0-9]{4})",
            # Date in format: 2017-01-02 // Month: jan
            "4": r"([0-9]{4})-([0-9]{2})-([0-9]{2})$",
            # Date in format: 2017.01.02 // Month: jan
            "5": r"([0-9]{4})\.([0-9]{2})\.([0-9]{2})$",
            # Date in format: 2017/01/02 // Month: jan
            "6": r"([0-9]{4})\/([0-9]{2})\/([0-9]{2})$",
            # Date in format: 2017.01.02 15:00:00
            "7": r"([0-9]{4})\.([0-9]{2})\.([0-9]{2})\s[0-9]{2}:[0-9]{2}:[0-9]{2}",
            # Date in format: 20170102 15:00:00 // Month: jan
            "8": r"([0-9]{4})([0-9]{2})([0-9]{2})\s[0-9]{2}:[0-9]{2}:[0-9]{2}",
            # Date in format: 2017-01-02 15:00:00 // Month: jan
            "9": r"([0-9]{4})-([0-9]{2})-([0-9]{2})\s[0-9]{2}:[0-9]{2}:[0-9]{2}",
            # Date in format: 02.01.2017 15:00:00 // Month: jan
            "10": r"([0-9]{2})\.([0-9]{2})\.([0-9]{4})\s[0-9]{2}:[0-9]{2}:[0-9]{2}",
            # Date in format: 02-Jan-2017 15:00:00 UTC
            "11": r"([0-9]{2})-([A-Z]{1}[a-z]{2})-([0-9]{4})\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s[A-Z]{1}.*",  # pylint: disable=line-too-long
            # Date in format: 2017/01/02 01:00:00 (+0900) // Month: jan
            "12": r"([0-9]{4})\/([0-9]{2})\/([0-9]{2})\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s\(.*\)",
            # Date in format: 2017/01/02 01:00:00 // Month: jan
            "13": r"([0-9]{4})\/([0-9]{2})\/([0-9]{2})\s[0-9]{2}:[0-9]{2}:[0-9]{2}$",
            # Date in format: Mon Jan 02 15:00:00 GMT 2017
            "14": r"[a-zA-Z]{3}\s([a-zA-Z]{3})\s([0-9]{2})\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s[A-Z]{3}\s([0-9]{4})",  # pylint: disable=line-too-long
            # Date in format: Mon Jan 02 2017
            "15": r"[a-zA-Z]{3}\s([a-zA-Z]{3})\s([0-9]{2})\s([0-9]{4})",
            # Date in format: 2017-01-02T15:00:00 // Month: jan
            "16": r"([0-9]{4})-([0-9]{2})-([0-9]{2})T[0-9]{2}:[0-9]{2}:[0-9]{2}$",
            # Date in format: 2017-01-02T15:00:00Z // Month: jan${'7}
            "17": r"([0-9]{4})-([0-9]{2})-([0-9]{2})T[0-9]{2}:[0-9]{2}:[0-9]{2}[A-Z].*",
            # Date in format: 2017-01-02T15:00:00+0200 // Month: jan
            "18": r"([0-9]{4})-([0-9]{2})-([0-9]{2})T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{4}",
            # Date in format: 2017-01-02T15:00:00+0200.622265+03:00 //
            # Month: jan
            "19": r"([0-9]{4})-([0-9]{2})-([0-9]{2})T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9].*[+-][0-9]{2}:[0-9]{2}",  # pylint: disable=line-too-long
            # Date in format: 2017-01-02T15:00:00+0200.622265 // Month: jan
            "20": r"([0-9]{4})-([0-9]{2})-([0-9]{2})T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}$",
            # Date in format: 2017-01-02T23:59:59.0Z // Month: jan
            "21": r"([0-9]{4})-([0-9]{2})-([0-9]{2})T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9].*[A-Z]",
            # Date in format: 02-01-2017 // Month: jan
            "22": r"([0-9]{2})-([0-9]{2})-([0-9]{4})",
            # Date in format: 2017. 01. 02. // Month: jan
            "23": r"([0-9]{4})\.\s([0-9]{2})\.\s([0-9]{2})\.",
            # Date in format: 2017-01-02T00:00:00+13:00 // Month: jan
            "24": r"([0-9]{4})-([0-9]{2})-([0-9]{2})T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}",  # pylint: disable=line-too-long
            # Date in format: 20170102 // Month: jan
            "25": r"(?=[0-9]{8})(?=([0-9]{4})([0-9]{2})([0-9]{2}))",
            # Date in format: 02-Jan-2017
            "26": r"([0-9]{2})-([A-Z]{1}[a-z]{2})-([0-9]{4})$",
            # Date in format: 02.1.2017 // Month: jan
            "27": r"([0-9]{2})\.([0-9]{1})\.([0-9]{4})",
            # Date in format: 02 Jan 2017
            "28": r"([0-9]{1,2})\s([A-Z]{1}[a-z]{2})\s([0-9]{4})",
            # Date in format: 02-January-2017
            "29": r"([0-9]{2})-([A-Z]{1}[a-z]*)-([0-9]{4})",
            # Date in format: 2017-Jan-02.
            "30": r"([0-9]{4})-([A-Z]{1}[a-z]{2})-([0-9]{2})\.",
            # Date in format: Mon Jan 02 15:00:00 2017
            "31": r"[a-zA-Z]{3}\s([a-zA-Z]{3})\s([0-9]{1,2})\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s([0-9]{4})",  # pylint: disable=line-too-long
            # Date in format: Mon Jan 2017 15:00:00
            "32": r"()[a-zA-Z]{3}\s([a-zA-Z]{3})\s([0-9]{4})\s[0-9]{2}:[0-9]{2}:[0-9]{2}",
            # Date in format: January 02 2017-Jan-02
            "33": r"([A-Z]{1}[a-z]*)\s([0-9]{1,2})\s([0-9]{4})",
            # Date in format: 2.1.2017 // Month: jan
            "34": r"([0-9]{1,2})\.([0-9]{1,2})\.([0-9]{4})",
            # Date in format: 20170102000000 // Month: jan
            "35": r"([0-9]{4})([0-9]{2})([0-9]{2})[0-9]+",
            # Date in format: 01/02/2017 // Month: jan
            "36": r"(0[1-9]|1[012])\/([0-3][0-9])\/([0-9]{4})",
            # Date in format: January  2 2017
            "37": r"([A-Z]{1}[a-z].*)\s\s([0-9]{1,2})\s([0-9]{4})",
            # Date in format: 2nd January 2017
            "38": r"([0-9]{1,})[a-z]{1,}\s([A-Z].*)\s(2[0-9]{3})",
        }

        for regx in regex_dates:
            # We loop through our map.

            # We try to get the matched groups if the date to convert match the currently
            # read regex.
            matched_result = Regex(
                date_to_convert, regex_dates[regx], return_data=True, rematch=True
            ).match()

            if matched_result:
                # The matched result is not None or an empty list.

                # We get the date.
                date = self._cases_management(regx, matched_result)

                if date:
                    # The date is given.

                    # We return the formatted date.
                    return "-".join(date)

        # We return an empty string as we were not eable to match the date format.
        return ""

    def _extract(self):  # pragma: no cover
        """
        Extract the expiration date from the whois record.

        :return: The status of the domain.
        :rtype: str
        """

        # We try to get the expiration date from the database.
        expiration_date_from_database = Whois().get_expiration_date()

        if expiration_date_from_database:
            # The hash of the current whois record did not changed and the
            # expiration date from the database is not empty not equal to
            # None or False.

            # We generate the files and print the status.
            # It's an active element!
            Generate(
                PyFunceble.STATUS["official"]["up"],
                "WHOIS",
                expiration_date_from_database,
            ).status_file()

            # We handle und return the official up status.
            return PyFunceble.STATUS["official"]["up"]

        # We get the whois record.
        self.whois_record = Lookup().whois(PyFunceble.INTERN["referer"])

        # We list the list of regex which will help us get an unformatted expiration date.
        to_match = [
            r"expire:(.*)",
            r"expire on:(.*)",
            r"Expiry Date:(.*)",
            r"free-date(.*)",
            r"expires:(.*)",
            r"Expiration date:(.*)",
            r"Expiry date:(.*)",
            r"Expire Date:(.*)",
            r"renewal date:(.*)",
            r"Expires:(.*)",
            r"validity:(.*)",
            r"Expiration Date             :(.*)",
            r"Expiry :(.*)",
            r"expires at:(.*)",
            r"domain_datebilleduntil:(.*)",
            r"Data de expiração \/ Expiration Date \(dd\/mm\/yyyy\):(.*)",
            r"Fecha de expiración \(Expiration date\):(.*)",
            r"\[Expires on\](.*)",
            r"Record expires on(.*)(\(YYYY-MM-DD\))",
            r"status:      OK-UNTIL(.*)",
            r"renewal:(.*)",
            r"expires............:(.*)",
            r"expire-date:(.*)",
            r"Exp date:(.*)",
            r"Valid-date(.*)",
            r"Expires On:(.*)",
            r"Fecha de vencimiento:(.*)",
            r"Expiration:.........(.*)",
            r"Fecha de Vencimiento:(.*)",
            r"Registry Expiry Date:(.*)",
            r"Expires on..............:(.*)",
            r"Expiration Time:(.*)",
            r"Expiration Date:(.*)",
            r"Expired:(.*)",
            r"Date d'expiration:(.*)",
            r"expiration date:(.*)",
        ]

        if self.whois_record:
            # The whois record is not empty.

            if "current_test_data" in PyFunceble.INTERN:
                # The end-user want more information whith his test.

                # We update the whois_record index.
                PyFunceble.INTERN["current_test_data"][
                    "whois_record"
                ] = self.whois_record

            for string in to_match:
                # We loop through the list of regex.

                # We try tro extract the expiration date from the WHOIS record.
                expiration_date = Regex(
                    self.whois_record, string, return_data=True, rematch=True, group=0
                ).match()

                if expiration_date:
                    # The expiration date could be extracted.

                    # We get the extracted expiration date.
                    self.expiration_date = expiration_date[0].strip()

                    # We initate a regex which will help us know if a number
                    # is present into the extracted expiration date.
                    regex_rumbers = r"[0-9]"

                    if Regex(
                        self.expiration_date, regex_rumbers, return_data=False
                    ).match():
                        # The extracted expiration date has a number.

                        # We format the extracted expiration date.
                        self.expiration_date = self._format()

                        if (
                            self.expiration_date
                            and not Regex(
                                self.expiration_date,
                                r"[0-9]{2}\-[a-z]{3}\-2[0-9]{3}",
                                return_data=False,
                            ).match()
                        ):
                            # The formatted expiration date does not match our unified format.

                            # We log the problem.
                            Logs().expiration_date(self.expiration_date)

                            # We log the whois record.
                            Logs().whois(self.whois_record)

                        if "current_test_data" in PyFunceble.INTERN:
                            # The end-user want more information whith his test.

                            # We update the expiration_date index.
                            PyFunceble.INTERN["current_test_data"][
                                "expiration_date"
                            ] = self.expiration_date

                        # We generate the files and print the status.
                        # It's an active element!
                        Generate(
                            PyFunceble.STATUS["official"]["up"],
                            "WHOIS",
                            self.expiration_date,
                        ).status_file()

                        # We log the whois record.
                        Logs().whois(self.whois_record)

                        # We save the whois record into the database.
                        Whois(expiration_date=self.expiration_date).add()

                        # We handle und return the official up status.
                        return PyFunceble.STATUS["official"]["up"]

                    # The extracted expiration date does not have a number.

                    # We log the whois record.
                    Logs().whois(self.whois_record)

                    # We return None, we could not get the expiration date.
                    return None

        # The whois record is empty.

        # We return None, we could not get the whois record.
        return None


#!/usr/bin/env python3

# pylint:disable=line-too-long
"""
The tool to check the availability or syntax of domains, IPv4 or URL.

::

    ██████╗ ██╗   ██╗███████╗██╗   ██╗███╗   ██╗ ██████╗███████╗██████╗ ██╗     ███████╗
    ██╔══██╗╚██╗ ██╔╝██╔════╝██║   ██║████╗  ██║██╔════╝██╔════╝██╔══██╗██║     ██╔════╝
    ██████╔╝ ╚████╔╝ █████╗  ██║   ██║██╔██╗ ██║██║     █████╗  ██████╔╝██║     █████╗
    ██╔═══╝   ╚██╔╝  ██╔══╝  ██║   ██║██║╚██╗██║██║     ██╔══╝  ██╔══██╗██║     ██╔══╝
    ██║        ██║   ██║     ╚██████╔╝██║ ╚████║╚██████╗███████╗██████╔╝███████╗███████╗
    ╚═╝        ╚═╝   ╚═╝      ╚═════╝ ╚═╝  ╚═══╝ ╚═════╝╚══════╝╚═════╝ ╚══════╝╚══════╝

This submodule will provide the production logic. We understand by production login
the logic to apply before commiting new code.

Author:
    Nissar Chababy, @funilrys, contactTATAfunilrysTODTODcom

Special thanks:
    https://pyfunceble.readthedocs.io/en/master/special-thanks.html

Contributors:
    http://pyfunceble.readthedocs.io/en/master/special-thanks.html

Project link:
    https://github.com/funilrys/PyFunceble

Project documentation:
    https://pyfunceble.readthedocs.io/en/master/

Project homepage:
    https://funilrys.github.io/PyFunceble/

License:
::

    MIT License

    Copyright (c) 2017-2019 Nissar Chababy

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
"""
# pylint: enable=line-too-long
# pylint: disable=bad-continuation

import PyFunceble
from PyFunceble.clean import Clean
from PyFunceble.config import Version
from PyFunceble.directory_structure import DirectoryStructure
from PyFunceble.helpers import Command, Dict, File, Regex

class Production:  # pylint: disable=too-few-public-methods
    """
    Manage and provide the production preparation logic.

    :param extern:
        Tell us if we do not have to execute the logic automatically.
        This allow method to be called.
    :type extern: bool
    """

    def __init__(self, extern=False):
        if not extern:
            # A method of this class is not called.

            if not self.is_dev_version() and not self.is_master_version():
                # The version is not the `dev` version nor the `master` version.

                # We raise an exception telling the user that there is a
                # problem somewhere around the versioning.
                raise Exception("Please switch to `dev` or `master` branch.")

            # We read and get the current content of `version.yaml`.
            self.data_version_yaml = self._get_current_version_yaml()

            # We split the version in oder to get only the list of digits from
            # the local version.
            self.version_yaml = Version(True).split_versions(
                self.data_version_yaml["current_version"]
            )

            # We we get the full version with the non-digits and the digits.
            self.current_version = Version(True).split_versions(
                PyFunceble.VERSION, True
            )

            if self._is_version_greater() or not Version(True).check_versions_literally(
                PyFunceble.VERSION, self.data_version_yaml["current_version"]
            ):
                # * The local version is greater than the older one.
                # or
                # * The local version is literally different than the
                # upstream one.

                # We clean the output directory.
                Clean(None)

                # We generate the productive directory structure file.
                DirectoryStructure(production=True)

                if self._does_require_deprecation():
                    # We have to put the previous version into the list of deprecated list.

                    # We reconstruct the version.
                    to_deprecate = ".".join(self.version_yaml)

                    # And we append it into the list of deprecated version.
                    self.data_version_yaml["deprecated"].append(to_deprecate)

                if self._does_require_force_update():
                    # We have to put the previous version into the list of forced for update list.

                    # We reconstruct the version.
                    to_force_update = ".".join(self.version_yaml)

                    # And we append it into the list of minimal version.
                    self.data_version_yaml["force_update"]["minimal_version"].append(
                        to_force_update
                    )

                if self.current_version[-1]:
                    # The non digit part of the version is not empty.

                    # We append it to the digit part.
                    self.current_version[0].append(self.current_version[-1])

                # We update the current version.
                self.data_version_yaml["current_version"] = ".".join(
                    self.current_version[0]
                )

                # We fix the urls in the README file.
                self._update_docs(PyFunceble.CURRENT_DIRECTORY + "README.rst")

                # We fix the urls in the configuration file.
                self._update_docs(
                    PyFunceble.CURRENT_DIRECTORY + ".PyFunceble_production.yaml"
                )

                # We fix the urls in the setup.py file.
                self._update_docs(PyFunceble.CURRENT_DIRECTORY + "setup.py")

                # We fix the urls in the documentation index.
                self._update_docs(
                    PyFunceble.CURRENT_DIRECTORY
                    + PyFunceble.directory_separator
                    + "docs"
                    + PyFunceble.directory_separator
                    + "index.rst"
                )

                # We fix the urls in the documentation logic representation.
                self._update_docs(
                    PyFunceble.CURRENT_DIRECTORY
                    + PyFunceble.directory_separator
                    + "docs"
                    + PyFunceble.directory_separator
                    + "logic-representation.rst"
                )

                # We fix the urls in the usage documentation.
                self._update_docs(
                    PyFunceble.CURRENT_DIRECTORY
                    + PyFunceble.directory_separator
                    + "docs"
                    + PyFunceble.directory_separator
                    + "usage"
                    + PyFunceble.directory_separator
                    + "from-a-terminal.rst"
                )

                # We fix the urls in the links configuration documentation.
                self._update_docs(
                    PyFunceble.CURRENT_DIRECTORY
                    + PyFunceble.directory_separator
                    + "docs"
                    + PyFunceble.directory_separator
                    + "configuration"
                    + PyFunceble.directory_separator
                    + "links.rst"
                )

                # We fix the urls in the code.
                self._update_code_urls()

                # We fix the setup.py.
                self._update_setup_py()

                # We fix the .travis.yml file.
                self._update_travis_yml()

                # We save our version data into our `version.yaml` file.
                Dict(self.data_version_yaml).to_yaml(
                    PyFunceble.CURRENT_DIRECTORY + "version.yaml", flow_style=None
                )

                # We prepare the message we are going to print on screen.
                message = (
                    PyFunceble.Fore.GREEN
                    + PyFunceble.Style.BRIGHT
                    + "We are ready to ship!! \n"
                )
                message += (
                    PyFunceble.Fore.CYAN
                    + "Please do not touch version.yaml nor setup.py (version update)"
                )  # pylint: disable=line-too-long

                # We print the message.
                print(message)

                # We exit the process.
                exit(0)
            else:
                # The local version is less or equal to the older one.

                # We print a message on screen.
                print(
                    PyFunceble.Fore.YELLOW
                    + PyFunceble.Style.BRIGHT
                    + "Are you sure that you did some changes ? Please update PyFunceble.VERSION if it is the case."  # pylint: disable=line-too-long
                )

                # We exit the process.
                exit(1)

    def _update_code_urls(self):
        """
        Read the code and update all links.
        """

        to_ignore = [".gitignore", ".keep"]

        for root, _, files in PyFunceble.walk(
            PyFunceble.CURRENT_DIRECTORY
            + PyFunceble.directory_separator
            + "PyFunceble"
            + PyFunceble.directory_separator
        ):
            # We loop through every directories and files in the `PyFunceble` directory.

            for file in files:
                # We loop through the list of files of the currently read directory.

                if file not in to_ignore and "__pycache__" not in root:
                    # * The filename is not into the list of file to ignore.
                    # and
                    # * The directory we are reading is not `__pycache__`.

                    if root.endswith(PyFunceble.directory_separator):
                        # The root directory ends with the directory separator.

                        # We fix the path in the currently read file.
                        self._update_docs(root + file)
                    else:
                        # The root directory does not ends with the directory separator.

                        # We fix the path in the currently read file.
                        # (after appending the directory separator between the root and file)
                        self._update_docs(root + PyFunceble.directory_separator + file)

        for root, _, files in PyFunceble.walk(
            PyFunceble.CURRENT_DIRECTORY
            + PyFunceble.directory_separator
            + "tests"
            + PyFunceble.directory_separator
        ):
            # We loop through every directories and files in the `tests` directory.
            for file in files:
                # We loop through the list of files of the currently read directory.

                if file not in to_ignore and "__pycache__" not in root:
                    # * The filename is not into the list of file to ignore.
                    # and
                    # * The directory we are reading is not `__pycache__`.

                    if root.endswith(PyFunceble.directory_separator):
                        # The root directory ends with the directory separator.

                        # We fix the path in the currently read file.
                        self._update_docs(root + file)
                    else:
                        # The root directory does not ends with the directory separator.

                        # We fix the path in the currently read file.
                        # (after appending the directory separator between the root and file)
                        self._update_docs(root + PyFunceble.directory_separator + file)

    @classmethod
    def _get_current_version_yaml(cls):
        """
        Get and return the content of version.yaml
        """

        return Dict().from_yaml(
            File(PyFunceble.CURRENT_DIRECTORY + "version.yaml").read()
        )

    def _is_version_greater(self):
        """
        Check if the current version is greater as the older older one.
        """

        # we compare the 2 versions.
        checked = Version(True).check_versions(
            self.current_version[0], self.version_yaml
        )

        if checked is not None and not checked:
            # The current version is greater as the older one.

            # We return True.
            return True

        # We return False
        return False

    @classmethod
    def is_dev_version(cls):
        """
        Check if the current branch is `dev`.
        """

        # We initiate the command we have to run in order to
        # get the branch we are currently working with.
        command = "git branch"

        # We execute and get the command output.
        command_result = Command(command).execute()

        for branch in command_result.split("\n"):
            # We loop through each line of the command output.

            if branch.startswith("*") and "dev" in branch:
                # The current branch is `dev`.

                # We return True.
                return True

        # The current branch is not `dev`.

        # We return False.
        return False

    @classmethod
    def is_master_version(cls):
        """
        Check if the current branch is `master`.
        """

        # We initiate the command we have to run in order to
        # get the branch we are currently working with.
        command = "git branch"

        # We execute and get the command output.
        command_result = Command(command).execute()

        for branch in command_result.split("\n"):
            # We loop through each line of the command output.

            if branch.startswith("*") and "master" in branch:
                # The current branch is `master`.

                # We return True.
                return True

        # The current branch is not `master`.

        # We return False.
        return False

    def _does_require_deprecation(self):
        """
        Check if we have to put the previous version into the deprecated list.
        """

        for index, version_number in enumerate(self.current_version[0][:2]):
            # We loop through the 2 last elements of the version.

            if version_number > self.version_yaml[index]:
                # The currently read version number is greater than the one we have in
                # the version.yaml.

                # We return True.
                return True

        # We return False, we do not need to deprecate anything.
        return False

    def _does_require_force_update(self):
        """
        Check if we have to put the previsous verion into the list of minimal version
        for force_update.
        """

        if self.current_version[0][0] > self.version_yaml[0]:
            # The current version first index is greater than the one we have in the
            # current version.yaml.

            # We return True.
            return True

        # We return False, we do not need to force the update for
        # the current version number.
        return False

    def _update_docs(self, file_to_update):
        """
        Update the given documentation file or :code:`README.rst` so that
        it always gives branch related URL and informations.

        .. note::
            This only apply to :code:`dev` and :code:`master` branch.

        :param file_to_update: The file to update.
        :type file_to_update: str
        """

        if self.is_dev_version():
            # The current version is the dev version.

            # We map what we have to replace.
            # Format: {match:replacement}
            regexes = {
                "/%s/" % "dev": r"\/%s\/" % "master",
                "=%s" % "dev": "=%s" % "master",
            }
        elif self.is_master_version():
            # The current version is the master version.

            # We map what we have to replace.
            regexes = {
                "/%s/" % "master": r"\/%s\/" % "dev",
                "=%s" % "master": "=%s" % "dev",
            }
        else:
            # The current version is not the master nor the dev version.

            # We raise an exception as the branch we are currently is not meaned
            # for production.
            raise Exception("Please switch to `dev` or `master` branch.")

        # We get the content of the file to fix.
        to_update = File(file_to_update).read()

        for replacement, regex in regexes.items():
            # We loop through reach element of the map.

            # We process the replacement.
            to_update = Regex(to_update, regex, replace_with=replacement).replace()

        # We finally overwrite the file to fix with the filtered.
        # content.
        File(file_to_update).write(to_update, overwrite=True)

    def _update_setup_py(self):
        """
        Update :code:`setup.py` so that it always have the right name.
        """

        # We initiate the path to the file we have to filter.
        setup_py_path = PyFunceble.CURRENT_DIRECTORY + "setup.py"

        if self.is_dev_version():
            # The current version is the `dev` version.

            # We map what we have to replace.
            # Format: {match:replacement}
            regexes = {
                'name="PyFunceble-dev"': r'name=".*"',
                '"Development Status :: 4 - Beta"': r'"Development\sStatus\s::.*"',
            }
        elif self.is_master_version():
            # The current version is the `dev` version.

            # We map what we have to replace.
            regexes = {
                'name="PyFunceble"': r'name=".*"',
                '"Development Status :: 5 - Production/Stable"': r'"Development\sStatus\s::.*"',
            }
        else:
            # The current version is not the `dev` nor the `master` version.

            # We raise an exception to the user, the current branch is not meant for
            # production.
            raise Exception("Please switch to `dev` or `master` branch.")

        # We get the file content.
        to_update = File(setup_py_path).read()

        for replacement, regex in regexes.items():
            # We loop through our map.

            # And we process the replacement.
            to_update = Regex(to_update, regex, replace_with=replacement).replace()

        # We finally replace the content of the file with the filtered
        # version.
        File(setup_py_path).write(to_update, overwrite=True)

    def _update_travis_yml(self):
        """
        Update :code:`.travis.yml` according to current branch.
        """

        # We initiate the file we have to filter/update.
        travis_yml_path = PyFunceble.CURRENT_DIRECTORY + ".travis.yml"

        if self.is_dev_version():
            # The current version is the `dev` version.

            # We map what we have to replace.
            # Format: {match:replacement}
            regexes = {
                "pip3 install pyfunceble-dev": r"pip3\sinstall\spyfunceble.*",
                "pip-autoremove pyfunceble-dev ": r"pip-autoremove\spyfunceble\s",
            }
        elif self.is_master_version():
            # The current version is the `master` version.

            # We map what we have to replace.
            regexes = {
                "pip3 install pyfunceble": r"pip3\sinstall\spyfunceble.*",
                "pip-autoremove pyfunceble ": r"pip-autoremove\spyfunceble[a-z-_]+\s",
            }
        else:
            # The current version is not the `master` nor the `dev` version.

            # We raise an exception, the current branch is not meant for production.
            raise Exception("Please switch to `dev` or `master` branch.")

        # We get the file content.
        to_update = File(travis_yml_path).read()

        for replacement, regex in regexes.items():
            # We loop through the map.

            # And we process the replacement.
            to_update = Regex(to_update, regex, replace_with=replacement).replace()

        # We finaly replace the file content with the filtered
        # content.
        File(travis_yml_path).write(to_update, overwrite=True)