"""
Copyright (c) 2017 James Patrick Dill, reshanie

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.
"""

import datetime
import logging
from collections import namedtuple
from json import JSONDecodeError

import faste

from . import utils, enums, errors

log = logging.getLogger("roblox")


class Asset(object):
    """Roblox Asset object.

    Use :meth:`RobloxSession.get_asset` to get a specific asset.

    Attributes
    ----------
    name : str
        Asset name
    description : str
        Asset description
    id : int
        Asset ID
    product_id : Optional[int]
        Product ID
    asset_type : :class:`roblox.AssetType`
        Asset type
    created : :class:`datetime.datetime`
        When the asset was first created
    updated : :class:`datetime.datetime`
        When the asset was last updated
    price : Optional[int]
        Price of the asset in ROBUX
    sales : Optional[int]
        Total sales of the asset
    is_new : bool
        Whether Roblox considers the asset 'new'
    for_sale : bool
        Whether asset can be taken/bought
    public_domain : bool
        If the asset is public domain / publicly viewable
    limited : bool
        If the asset is limited
    limited_unique : bool
        If the asset is limited and unique
    remaining : Optional[int]
        How many are remaining, if the asset is limited
    membership_level: :class:`roblox.Membership`
        Minimum Builders Club needed to take the asset
    """

    def __init__(self, client, asset_id=0):
        """param client: client
        :type client: roblox.RobloxSession
        """

        self.client = client

        self.id = asset_id

        self._update_info()

    def _update_info(self):
        try:
            product_info = self.client.http.product_info(self.id)
        except JSONDecodeError:
            raise errors.BadRequest("Invalid asset, possibly deleted")

        self.product_id = product_info.get("ProductId")
        self.name = product_info.get("Name")
        self.description = product_info.get("Description")

        self.asset_type = enums.AssetType(product_info.get("AssetTypeId"))
        self.icon_image_asset_id = product_info.get("IconImageAssetId")

        self.created = utils.get_datetime(product_info.get("Created"))
        self.updated = utils.get_datetime(product_info.get("Updated"))

        self.price = product_info.get("PriceInRobux")
        self.sales = product_info.get("Sales")
        self.is_new = product_info.get("IsNew")
        self.for_sale = product_info.get("IsForSale")
        self.public_domain = product_info.get("IsPublicDomain")

        self.unique = product_info.get("IsLimitedUnique")
        self.limited = product_info.get("IsLimited") or self.unique
        self.remaining = product_info.get("Remaining")

        self.membership_level = enums.Membership(product_info.get("MinimumMembershipLevel"))

        self.creator_id = product_info["Creator"]["CreatorTargetId"]
        self.creator_type = product_info["Creator"]["CreatorType"]

    def __hash__(self):
        return self.id

    def __repr__(self):
        return "<roblox.Asset {0.asset_type.name} name={0.name!r} id={0.id!r}>".format(self)

    def __str__(self):
        return self.name

    def __eq__(self, other):
        """
        Returns True if two asset objects are the same asset.
        """
        if type(other) != Asset:
            return False
        return self.id == other.id

    @property
    @faste.decor.rr_cache()
    def creator(self):
        """Asset creator

        :returns: :class:`User` or :class:`Group`"""
        if self.creator_type == "User":
            return self.client.get_user(user_id=self.creator_id)
        else:
            return self.client.get_group(self.creator_id)

    def buy(self):
        """
        Takes/buys asset.

        :returns: `True` if successful
        """
        return self.client.http.buy_product(self.product_id,
                                            self.price,
                                            self.creator_id)

    def remove_from_inventory(self):
        """
        Deletes asset from inventory of client user.

        :returns: `True` if successful
        """
        return self.client.http.delete_from_inventory(self.id)

    def post_comment(self, content):
        """
        Posts comment on asset

        :param str content: Comment text

        :return: :class:`Comment`
        """
        if not content:
            raise errors.BadRequest("Comment must have text.")
        comment = self.client.http.post_comment(self.id, content)

        return Comment(self, content=comment["Text"], created=comment["PostedDate"], author=self.client.me)

    def owned_by(self, user):
        """
        Checks if asset is owned by user.

        :param user: User
        :type user: :class:`User`
        :returns: `True` if user owns asset
        """
        return self.client.http.user_owns_asset(user.id, self.id)

    @property
    @faste.decor.rr_cache()
    def icon(self):
        """Asset for icon

        :returns: Optional[:class:`Asset`]"""
        if self.icon_image_asset_id == 0:
            return None

        return self.client.get_asset(self.icon_image_asset_id)

    @property
    def favorites(self):
        """Favorite count of asset

        :returns: int"""
        return self.client.http.asset_favorites(self.id)

    def is_favorited(self):
        """Whether asset is favorited by client

        :returns: bool"""
        return self.client.http.is_favorited(self.id)

    def favorite(self):
        """Favorites asset if it isn't favorited already.

        :returns: return value of :meth:`is_favorited` (bool)"""
        if self.is_favorited():
            return True

        return self.client.http.toggle_favorite(self.id)

    def unfavorite(self):
        """Unfavorites asset if it's favorited.

        :returns: return value of :meth:`is_favorited` (bool)"""
        if not self.is_favorited():
            return False

        return not self.client.http.toggle_favorite(self.id)

    def recent_average_price(self):
        """Gets RAP of asset, if it is a collectible.

        :returns: Optional[`int`]"""
        return self.client.http.get_sales_data(self.id).get("AveragePrice")

    def RAP(self):
        """Alias for :meth:recent_average_pice"""
        return self.recent_average_price()

    def sales_chart(self):
        """Gets :class:`SalesChart` for asset, if it's a collectible."""

        return SalesChart(self.client, self)


class Game(Asset):
    pass


sales_point = namedtuple("sales_day", "date price volume")


class SalesChart(object):
    """Asset sales chart, representing user sales of a collectible.

    You can also iterate over this object, and index it. ``SalesChart[0]`` will return the first sales point.
    You can also use ::
        >>> list(chart)
        >>> reversed(chart)
        >>> dict(chart)
        >>> len(chart)
        >>> datetime.date in chart

    The dict version and list versions' values are namedtuples representing sales points, with ``sales_point.date``
    , ``sales_point.price`` , and ``sales_point.volume``

    The dict's keys are :class:`datetime.date`

    Attributes
    ----------
    asset : :class:`Asset`
        Asset the sales chart belongs to
    chart_dict : dict
        dict version of the sales chart
    """

    def __init__(self, client, asset):
        self.client = client
        self.asset = asset

        self.chart_dict = self._chart_dict()

    def _chart_dict(self):
        sales_data = self.client.http.get_sales_data(self.asset.id)
        if not sales_data:
            raise ValueError("{!r} isn't a collectible and has no sales data".format(self.asset))

        sales_chart = sales_data.get("HundredEightyDaySalesChart").split("|")
        volume_chart = sales_data.get("HundredEightyDayVolumeChart").split("|")

        sales_chart_dict = {}
        for sale in sales_chart:
            ts = sale.split(",")
            if not ts[0]:
                break

            k = int(ts[0][:-3])
            sales_chart_dict[k] = int(ts[1])

        volume_chart_dict = {}
        for vol in volume_chart:
            tv = vol.split(",")
            if not tv[0]:
                break

            k = int(tv[0][:-3])
            volume_chart_dict[k] = int(tv[1])

        rtd = {}
        for timestamp in sales_chart_dict:
            nts = datetime.date.fromtimestamp(timestamp)

            rtd[nts] = sales_point(
                date=nts,
                price=sales_chart_dict.get(timestamp),
                volume=volume_chart_dict.get(timestamp) or 0,
            )

        return rtd

    def __dict__(self):
        return self.chart_dict

    def __iter__(self):
        return list(self.chart_dict.values())

    def __getitem__(self, index):
        return list(self.chart_dict.values())[index]

    def __len__(self):
        return len(self.chart_dict)

    def __reversed__(self):
        return reversed(list(self.chart_dict.values()))

    def __contains__(self, item):
        if isinstance(item, datetime.date):
            return item in self.chart_dict.keys()
        elif isinstance(item, sales_point):
            return item in self.chart_dict.values()
        return False

    def __repr__(self):
        return "<roblox.SalesChart asset={0.asset.name!r}>".format(self)


class Comment(object):
    """Asset comment.

    Attributes
    ----------
    asset : :class:`Asset`
        Asset the comment belongs to
    content : str
        Comment content
    created : :class:`datetime.datetime`
        When the comment was posted"""

    __slots__ = ["asset", "content", "created", "_user", "_user_cache"]

    def __init__(self, asset, content=None, created=None, author=None):
        """
        :type asset: :class:`Asset`
        """
        self.asset = asset

        self.content = content
        self.created = utils.get_datetime(created) if created else None

        self._user = author
        self._user_cache = None

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            return False

        return self.asset == other.asset and self.created == other.created and self.author == other.author

    def __repr__(self):
        return "<roblox.Comment asset={0.asset.name!r} author={0.author!r} created={0.created!r}>".format(self)

    def __str__(self):
        return self.content

    @property
    @faste.decor.rr_cache()
    def author(self):
        """User who made the post.

        :returns: :class:`User`"""

        if type(self._user) == int:
            return self.asset.client.get_user(user_id=self._user)
        elif type(self._user) == str:
            return self.asset.client.get_user(username=self._user)

        return self._user

