# This code is free software; you can redistribute it and/or modify it under
# the terms of the new BSD License.
#
# Copyright (c) 2008-2015, Sebastian Staudt

require 'cgi'
require 'time'

require 'steam-condenser/community/cacheable'
require 'steam-condenser/community/game_stats'
require 'steam-condenser/community/steam_game'
require 'steam-condenser/community/steam_group'
require 'steam-condenser/community/web_api'
require 'steam-condenser/community/xml_data'
require 'steam-condenser/error'

module SteamCondenser::Community

  # The SteamId class represents a Steam Community profile (also called Steam
  # ID)
  #
  # @author Sebastian Staudt
  class SteamId

    include Cacheable
    cacheable_with_ids :custom_url, :steam_id64

    include XMLData

    # Returns the custom URL of this Steam ID
    #
    # The custom URL is a user specified unique string that can be used instead
    # of the 64bit SteamID as an identifier for a Steam ID.
    #
    # @note The custom URL is not necessarily the same as the user's nickname.
    # @return [String] The custom URL of this Steam ID
    attr_reader :custom_url

    # Returns the groups this user is a member of
    #
    # @return [Array<SteamGroup>] The groups this user is a member of
    attr_reader :groups

    # Returns the location of the user
    #
    # @return [String] The location of the user
    attr_reader :location

    # Returns the date of registration for the Steam account belonging to this
    # SteamID
    #
    # @return [Time] The date of the Steam account registration
    attr_reader :member_since

    # Returns the Steam nickname of the user
    #
    # @return [String] The Steam nickname of the user
    attr_reader :nickname

    # Returns the privacy state of this Steam ID
    #
    # @return [String] The privacy state of this Steam ID
    attr_reader :privacy_state

    # Returns the real name of this user
    #
    # @return [String] The real name of this user
    attr_reader :real_name

    # Returns the message corresponding to this user's online state
    #
    # @return [String] The message corresponding to this user's online state
    # @see #ingame?
    # @see #online?
    attr_reader :state_message

    # Returns the summary this user has provided
    #
    # @return [String] This user's summary
    attr_reader :summary

    # Returns this user's ban state in Steam's trading system
    #
    # @return [String] This user's trading ban state
    attr_reader :trade_ban_state

    # Returns the visibility state of this Steam ID
    #
    # @return [Fixnum] This Steam ID's visibility State
    attr_reader :visibility_state

    # Converts a 64bit numeric SteamID as used by the Steam Community to the
    # legacy SteamID format
    #
    # @param [Fixnum] community_id The SteamID string as used by the Steam
    #        Community
    # @raise [Error] if the community ID is to small
    # @return [String] The converted SteamID, like `STEAM_0:0:12345`
    def self.community_id_to_steam_id(community_id)
      steam_id1 = community_id % 2
      steam_id2 = community_id - 76561197960265728

      unless steam_id2 > 0
        raise SteamCondenser::Error, "SteamID #{community_id} is too small."
      end

      steam_id2 = (steam_id2 - steam_id1) / 2

      "STEAM_0:#{steam_id1}:#{steam_id2}"
    end

    # Converts a 64bit numeric SteamID as used by the Steam Community to the
    # modern SteamID format (also known as SteamID 3)
    #
    # @param [Fixnum] community_id The SteamID string as used by the Steam
    #        Community
    # @raise [Error] if the community ID is to small
    # @return [String] The converted SteamID, like `[U:1:12345]`
    def self.community_id_to_steam_id3(community_id)
      # Only the public universe (1) is supported
      steam_id1 = 1
      steam_id2 = community_id - 76561197960265728

      unless steam_id2 > 0
        raise SteamCondenser::Error, "SteamID #{community_id} is too small."
      end

      "[U:#{steam_id1}:#{steam_id2}]"
    end

    # Resolves a vanity URL of a Steam Community profile to a 64bit numeric
    # SteamID
    #
    # @param [String] vanity_url The vanity URL of a Steam Community profile
    # @return [Fixnum] The 64bit numeric SteamID
    def self.resolve_vanity_url(vanity_url)
      json = WebApi.json 'ISteamUser', 'ResolveVanityURL', 1, vanityurl: vanity_url
      result = json[:response]

      return nil if result[:success] != 1

      result[:steamid].to_i
    end

    # Converts a SteamID as reported by game servers to a 64bit numeric SteamID
    # as used by the Steam Community
    #
    # @param [String] steam_id The SteamID string as used on servers, like
    #        `STEAM_0:0:12345`
    # @raise [Error] if the SteamID doesn't have the correct format
    # @return [Fixnum] The converted 64bit numeric SteamID
    def self.steam_id_to_community_id(steam_id)
      if steam_id == 'STEAM_ID_LAN' || steam_id == 'BOT'
        raise SteamCondenser::Error, "Cannot convert SteamID \"#{steam_id}\" to a community ID."
      elsif steam_id =~ /^STEAM_[0-1]:([0-1]:[0-9]+)$/
        steam_id = $1.split(':').map! { |s| s.to_i }
        steam_id[0] + steam_id[1] * 2 + 76561197960265728
      elsif steam_id =~ /^\[U:([0-1]:[0-9]+)\]$/
        steam_id = $1.split(':').map { |s| s.to_i }
        steam_id[0] + steam_id[1] + 76561197960265727
      else
        raise SteamCondenser::Error, "SteamID \"#{steam_id}\" doesn't have the correct format."
      end
    end

    # Creates a new `SteamId` instance using a SteamID as used on servers
    #
    # The SteamID from the server is converted into a 64bit numeric SteamID
    # first before this is used to retrieve the corresponding Steam Community
    # profile.
    #
    # @param [String] steam_id The SteamID string as used on servers, like
    #        `STEAM_0:0:12345`
    # @return [SteamId] The `SteamId` belonging to the given SteamID
    # @see .convert_steam_id_to_community_id
    # @see #initialize
    def self.from_steam_id(steam_id)
      new(steam_id_to_community_id steam_id)
    end

    # Creates a new `SteamId` instance for the given Steam ID
    #
    # @param [String, Fixnum] id The custom URL of the Steam ID specified by the
    #        user or the 64bit SteamID
    # @macro cacheable
    def initialize(id)
      if id.is_a? Numeric
        @steam_id64 = id
      else
        if id =~ /^STEAM_[0-1]:[0-1]:[0-9]+$/ || id =~ /\[U:[0-1]:[0-9]+\]/
          @steam_id64 = SteamId.steam_id_to_community_id id
        else
          @custom_url = id.downcase
        end
      end
    end

    # Returns whether the owner of this SteamID is VAC banned
    #
    # @return [Boolean] `true` if the user has been banned by VAC
    def banned?
      @vac_banned
    end
    alias_method :is_banned?, :banned?

    # Returns the base URL for this Steam ID
    #
    # This URL is different for Steam IDs having a custom URL.
    #
    # @return [String] The base URL for this SteamID
    def base_url
      if @custom_url.nil?
        "http://steamcommunity.com/profiles/#@steam_id64"
      else
        "http://steamcommunity.com/id/#@custom_url"
      end
    end

    # Fetches data from the Steam Community by querying the XML version of the
    # profile specified by the ID of this Steam ID
    #
    # @raise [Error] if the Steam ID data is not available, e.g. when it is
    #        private
    # @see Cacheable#fetch
    def fetch
      profile = parse "#{base_url}?xml=1"

      raise SteamCondenser::Error, profile['error'] unless profile['error'].nil?

      unless profile['privacyMessage'].nil?
        raise SteamCondenser::Error, profile['privacyMessage']
      end

      @nickname         = CGI.unescapeHTML profile['steamID']
      @steam_id64       = profile['steamID64'].to_i
      @limited          = (profile['isLimitedAccount'].to_i == 1)
      @trade_ban_state  = profile['tradeBanState']
      @vac_banned       = (profile['vacBanned'].to_i == 1)

      @image_url        = profile['avatarIcon'][0..-5]
      @online_state     = profile['onlineState']
      @privacy_state    = profile['privacyState']
      @state_message    = profile['stateMessage']
      @visibility_state = profile['visibilityState'].to_i

      if public?
        @custom_url = profile['customURL']
        @custom_url.downcase! unless @custom_url.nil?

        @location     = profile['location']
        @member_since = Time.parse profile['memberSince']
        @real_name    = CGI.unescapeHTML profile['realname'] || ''
        @summary      = CGI.unescapeHTML profile['summary'] || ''
      end
    rescue
      raise $! if $!.is_a? SteamCondenser::Error
      raise SteamCondenser::Error, 'XML data could not be parsed.'
    end

    # Fetches the friends of this user
    #
    # This creates a new `SteamId` instance for each of the friends without
    # fetching their data.
    #
    # @see #friends
    # @see #initialize
    def fetch_friends
      params = { relationship: 'friend', steamid: steam_id64 }

      friends_data = WebApi.json 'ISteamUser', 'GetFriendList', 1, params
      @friends = friends_data[:friendslist][:friends].map do |friend|
        SteamId.new(friend[:steamid].to_i, false)
      end
    end

    # Fetches the games this user owns
    #
    # This fills the game hash with the names of the games as keys. The values
    # will either be `false` if the game does not have stats or the game's
    # "friendly name".
    #
    # @see #games
    def fetch_games
      params = {
        include_appinfo: 1,
        include_played_free_games: 1,
        steamId: steam_id64
      }
      games_data = WebApi.json 'IPlayerService', 'GetOwnedGames', 1, params
      @games            = {}
      @recent_playtimes = {}
      @total_playtimes  = {}
      games_data[:response][:games].each do |game_data|
        app_id = game_data[:appid]
        @games[app_id] = SteamGame.new app_id, game_data

        @recent_playtimes[app_id] = game_data[:playtime_2weeks] || 0
        @total_playtimes[app_id]  = game_data[:playtime_forever] || 0
      end

      @games
    end

    # Fetches the groups this user is member of
    #
    # Uses the ISteamUser/GetUserGroupList interface.
    #
    # @return [Array<SteamGroup>] The groups of this user
    # @see #groups
    def fetch_groups
      groups_data = WebApi.json 'ISteamUser', 'GetUserGroupList', 1, steamid: steam_id64

      @groups = []
      groups_data[:response][:groups].each do |group_data|
        @groups << SteamGroup.new(group_data[:gid].to_i, false)
      end

      @groups
    end

    # Fetches information about the game the user is playing currently
    #
    # @return The user’s current game information
    # @see #game_info
    def fetch_game_info
      params = { :steamids => steam_id64 }
      summary_data = WebApi.json 'ISteamUser', 'GetPlayerSummaries', 2, params
      data = summary_data[:response][:players].first || {}

      @game_info = {
        :game_id => data[:gameid],
        :game_name => data[:gameextrainfo],
        :game_server_ip => data[:gameserverip],
        :game_server_id => data[:gameserversteamid]
      }
    end

    # Returns the URL of the full-sized version of this user's avatar
    #
    # @return [String] The URL of the full-sized avatar
    def full_avatar_url
      "#{@image_url}_full.jpg"
    end

    # Returns the stats for the given game for the owner of this SteamID
    #
    # @param [Fixnum] app_id The application ID of the game stats should be
    #        fetched for
    # @return [GameStats] The statistics for the game with the given name
    # @raise [Error] if the user does not own this game or it does not have any
    #        stats
    def game_stats(app_id)
      GameStats.new @custom_url || @steam_id64, app_id
    end

    # Returns the Steam Community friends of this user
    #
    # If the friends haven't been fetched yet, this is done now.
    #
    # @return [Array<SteamId>] The friends of this user
    # @see #fetch_friends
    def friends
      @friends || fetch_friends
    end

    # Returns the games this user owns
    #
    # The keys of the hash are the games' application IDs and the values are
    # the corresponding game instances.
    #
    # If the friends haven't been fetched yet, this is done now.
    #
    # @return [Hash<Fixnum, SteamGame>] The games this user owns
    # @see #fetch_games
    def games
      @games || fetch_games
    end

    # Returns all groups where this user is a member
    #
    # @return [] The groups of this user
    # @see #fetch_groups
    def groups
      @groups || fetch_groups
    end

    # Returns information about the game the user is playing currently
    #
    # If the information hasn't been fetched yet, this is done now.
    #
    # @return [Hash<Symbol, Object>] The user’s current game information
    # @see #fetch_game_info
    def game_info
      @game_info || fetch_game_info
    end

    # Returns the URL of the icon version of this user's avatar
    #
    # @return [String] The URL of the icon-sized avatar
    def icon_url
      "#@image_url.jpg"
    end

    # Returns a unique identifier for this Steam ID
    #
    # This is either the 64bit numeric SteamID or custom URL
    #
    # @return [Fixnum, String] The 64bit numeric SteamID or the custom URL
    def id
      @custom_url || @steam_id64
    end

    # Returns whether the owner of this SteamId is playing a game
    #
    # @return [Boolean] `true` if the user is in-game
    def in_game?
      @online_state == 'in-game'
    end

    # Returns whether this Steam account is limited
    #
    # @return [Boolean] `true` if this account is limited
    def limited?
      @limited
    end

    # Returns the URL of the medium-sized version of this user's avatar
    #
    # @return [String] The URL of the medium-sized avatar
    def medium_avatar_url
      "#{@image_url}_medium.jpg"
    end

    # Returns whether the owner of this SteamID is currently logged into Steam
    #
    # @return [Boolean] `true` if the user is online
    def online?
      @online_state != 'offline'
    end

    # Returns whether this Steam ID is publicly accessible
    #
    # @return [Boolean] `true` if this Steam ID is public
    def public?
      @privacy_state == 'public'
    end

    # Returns the time in minutes this user has played this game in the last
    # two weeks
    #
    # @param [Fixnum] app_id The application ID of the game
    # @return [Fixnum] The number of minutes this user played the given game in
    #         the last two weeks
    def recent_playtime(app_id)
      @recent_playtimes[app_id]
    end

    # Returns this user's 64bit SteamID
    #
    # If the SteamID is not known yet it is resolved from the vanity URL.
    #
    # @return [Fixnum] This user's 64bit SteamID
    # @see .resolve_vanity_url
    def steam_id64
      @steam_id64 ||= self.class.resolve_vanity_url(@custom_url)
    end

    # Returns the current Steam level of this user
    #
    # If the Steam level hasn't been updated yet, this is done now.
    #
    # @return [Fixnum] The current Steam level of this user
    # @see #update_steam_level
    def steam_level
      @steam_level || update_steam_level
    end

    # Returns the total time in minutes this user has played this game
    #
    # @param [Fixnum] app_id The application ID of the game
    # @return [Fixnum] The total number of minutes this user played the given
    #         game
    def total_playtime(app_id)
      @total_playtimes[app_id]
    end

    # Updates the Steam level of this user using the Web API
    #
    # @return [Fixnum] The current Steam level of this user
    # @see #steam_level
    def update_steam_level
      data = WebApi.json 'IPlayerService', 'GetSteamLevel', 1, steamid: @steam_id64
      @steam_level = data[:response][:player_level]
    end

  end
end
