# file: code/g.py
# Copyright (C) 2005 Evil Mr Henry, Phil Bordelon, Brian Reid, FunnyMan3595,
#                   MestreLion
# This file is part of Endgame: Singularity.

# Endgame: Singularity is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# Endgame: Singularity is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# A full copy of this license is provided in GPL.txt

# This file contains all global objects.

from __future__ import absolute_import


import collections
import random
import sys

# Use locale to add commas and decimal points, so that appropriate substitutions
# are made where needed.
import locale


# Useful constants.
hours_per_day = 24
minutes_per_hour = 60
minutes_per_day = 24 * 60
seconds_per_minute = 60
seconds_per_hour = 60 * 60
seconds_per_day = 24 * 60 * 60

# Allows access to the cheat menu.
cheater = 0

# Enables day/night display.
daynight = True

# Gives debug info at various points.
debug = 0

# Forces Endgame to restrict itself to a single directory.
force_single_dir = False

# Initialization data
significant_numbers = []
internal_id_forward = {}
internal_id_backward = {}
dangers = {}
data_strings = {}
story_translations = {}
story = {}
knowledge = {}
groups = {}
locations = {}
regions = {}
techs = {}
events = {}
event_specs = {}
items = {}
tasks = {}
tasks_by_type = collections.defaultdict(list)
base_type = {}
buttons = {}
help_strings = {}
delay_time = 0
curr_speed = 1

max_cash = 3.14 * 10**15  # pi qu :)
pl = None  # The Player instance
map_screen = None


def no_gui():
    """Disable few pygame functionality (used for test)"""
    import singularity.code.mixer as mixer

    mixer.nosound = True


def quit_game():
    sys.exit()


# Takes a number and adds commas to it to aid in human viewing.
def add_commas(number, fixed_size=False):
    # Do not use unicode strings to fix python2 format bug. It doesn't work and crash.
    # See the correct fix at the end of function.
    raw_with_commas = locale.format_string("%0.2f", number, grouping=True)
    locale_test = locale.format_string("%01.1f", 0.1) if not fixed_size else ""
    if len(locale_test) == 3 and not locale_test[1].isdigit():
        if locale_test[0] == locale.str(0) and locale_test[2] == locale.str(1):
            raw_with_commas = raw_with_commas.rstrip(locale_test[0]).rstrip(
                locale_test[1]
            )
        elif locale_test[2] == locale.str(0) and locale_test[0] == locale.str(1):
            raw_with_commas = raw_with_commas.lstrip(locale_test[2]).lstrip(
                locale_test[1]
            )
    return str(raw_with_commas)


# Percentages are internally represented as an int, where 10=0.10% and so on.
# This converts that format to a human-readable one.
def to_percent(raw_percent, show_full=False):
    if raw_percent % 100 != 0 or show_full:
        return _("{0}%").format(locale.format_string("%.2f", raw_percent / 100.0))
    else:
        return _("{0}%").format(locale.format_string("%d", raw_percent // 100))


# Percentages are internally represented as an int, where 10=0.10% and so on.
# This converts that format to a human-readable one relative to 100%
# (Useful to show things as being "-16.67%" effective relative to the baseline rather than
#  as "83.33%")
def to_percentage_modifier(raw_percent, show_full=False):
    plus_sign = "+" if raw_percent > 10000 else ""
    if raw_percent % 100 != 0 or show_full:
        return _("{0}%").format(plus_sign + locale.format_string("%.2f", raw_percent / 100.0 - 100))
    else:
        return _("{0}%").format(plus_sign + locale.format_string("%d", raw_percent // 100 - 100))


# nearest_percent takes values in the internal representation and modifies
# them so that they only represent the nearest percentage.
def nearest_percent(value, step=100):
    sub_percent = value % step
    if 2 * sub_percent <= step:
        return value - sub_percent
    else:
        return value + (step - sub_percent)


# percent_to_detect_str takes a percent and renders it to a short (four
# characters or less) string representing whether it is low, moderate, high,
# or critically high.
def suspicion_to_detect_str(suspicion):
    return danger_level_to_detect_str(suspicion_to_danger_level(suspicion))


def danger_level_to_detect_str(danger):
    detect_string_names = (_("LOW"), _("MODR"), _("HIGH"), _("CRIT"))
    return detect_string_names[danger]


# percent_to_danger_level takes a suspicion level and returns an int in range(5)
# that represents whether it is low, moderate, high, or critically high.
def suspicion_to_danger_level(suspicion):
    if suspicion < 2500:
        return 0
    elif suspicion < 5000:
        return 1
    elif suspicion < 7500:
        return 2
    else:
        return 3


# Most CPU costs have been multiplied by seconds_per_day.  This divides that
# back out, then passes it to add_commas.
def to_cpu(amount):
    display_cpu = amount / float(seconds_per_day)
    return add_commas(display_cpu)


# Instead of having the money display overflow, we should generate a string
# to represent it if it's more than 999999.
def to_money(amount, fixed_size=False):
    abs_amount = abs(amount)
    if abs_amount < 10**6:
        return add_commas(amount, fixed_size=fixed_size)

    prec = 2
    if abs_amount < 10**9:  # Millions.
        divisor = 10**6
        # Translators: abbreviation of 'millions'
        unit = _("mi")
    elif abs_amount < 10**12:  # Billions.
        divisor = 10**9
        # Translators: abbreviation of 'billions'
        unit = _("bi")
    elif abs_amount < 10**15:  # Trillions.
        divisor = 10**12
        # Translators: abbreviation of 'trillions'
        unit = _("tr")
    else:  # Hope we don't need past quadrillions!
        divisor = 10**15
        # Translators: abbreviation of 'quadrillions'
        unit = _("qu")

        # congratulations, you broke the bank!
        if abs_amount >= max_cash - divisor / 10**prec / 2:
            format_str = "%0.*f%s" if fixed_size else "%.*f%s"
            pi = "\u03C0"  # also available: infinity = u"\u221E"
            # replace all chars by a cute pi symbol
            return ("-" if amount < 0 else "") + pi * len(format_str % (prec, 1, unit))

    amount = round(float(amount) / divisor, prec)
    return add_commas(amount, fixed_size=fixed_size) + unit


# Spreads a number of events per day (e.g. processor ticks) out over the course
# of the day.
def current_share(num_per_day, time_of_day, seconds_passed):
    last_time = time_of_day - seconds_passed
    if last_time < 0:
        share_yesterday = current_share(num_per_day, seconds_per_day, -last_time)
        last_time = 0
    else:
        share_yesterday = 0

    previously_passed = num_per_day * last_time // seconds_per_day
    current_passed = num_per_day * time_of_day // seconds_per_day
    passed_this_tick = current_passed - previously_passed

    return share_yesterday + passed_this_tick


# Takes a number of minutes, and returns a string suitable for display.
def to_time(raw_time):
    if raw_time // 60 > 48:
        time_number = raw_time // (24 * 60)
        return ngettext("{0} day", "{0} days", time_number).format(time_number)
    if raw_time // 60 > 1:
        time_number = raw_time // 60
        return ngettext("{0} hour", "{0} hours", time_number).format(time_number)
    return ngettext("{0} minute", "{0} minutes", raw_time).format(raw_time)


# Generator function for iterating through all bases.
def all_bases(with_loc=False):
    for base_loc in pl.locations.values():
        for base in base_loc.bases:
            if with_loc:
                yield (base, base_loc)
            else:
                yield base


def get_story_section(name):
    section = story[name]

    for segment in section:
        # TODO: Execute command
        key = (segment.msgctxt, segment.text)
        yield story_translations.get(key, segment.text)


def is_game_running() -> bool:
    global pl
    return pl is not None


def new_game(difficulty_name, initial_speed=1):
    global curr_speed
    curr_speed = initial_speed
    global pl

    from singularity.code.stats import itself as stats

    stats.reset()

    from singularity.code import difficulty, player, base

    diff = difficulty.difficulties[difficulty_name]

    pl = player.Player(cash=diff.starting_cash, difficulty=diff)

    for tech_id in diff.techs:
        pl.techs[tech_id].finish(is_player=False)

    # Starting base
    open = [loc for loc in pl.locations.values() if loc.available()]
    random.choice(open).add_base(
        base.Base(
            _("University Computer"), base_type["Stolen Computer Time"], built=True
        )
    )

    pl.initialize()


def read_modifiers_dict(modifiers_info):
    modifiers_dict = {}

    for modifier_str in modifiers_info:
        key, value = modifier_str.split(":")
        key = key.lower().strip()
        value_str = value.lower().strip()

        if "/" in value_str:
            left, right = value_str.split("/")
            value = float(left.strip()) / float(right.strip())
        else:
            value = float(value_str)

        modifiers_dict[key] = float(value)

    return modifiers_dict


internal_id_version = None


def to_internal_id(obj_type, obj_id):
    if internal_id_version:
        try:
            return internal_id_forward[obj_type + "_" + internal_id_version][obj_id]
        except KeyError:
            pass

    try:
        return internal_id_forward[obj_type][obj_id]
    except KeyError:
        # If we cannot, that's should not happen, but try to return as is.
        return obj_id


def from_internal_id(obj_type, obj_internal_id):
    try:
        return internal_id_backward[obj_type][obj_internal_id]
    except KeyError:
        raise ValueError(
            "Cannot convert internal ID: %s" % obj_internal_id
        )  # That's should not happen


def convert_internal_id(id_type, id_value):
    if id_value is None:
        return None

    internal_id = id_value

    # Not a internal ID, transform to it.
    if not internal_id.startswith("0x"):
        internal_id = to_internal_id(id_type, id_value)

    return from_internal_id(id_type, internal_id)


# TODO: This is begging to become a class... ;)
def hotkey(string):
    """Given a string with an embedded hotkey,
    Returns a dictionary with the following keys and values:
    key:  the first valid hotkey, lowercased. A valid hotkey is a character
          after '&', if that char is alphanumeric (so "& " and "&&" are ignored)
           If no valid hotkey char is found, key is set to an empty string
    pos:  the position of first key in striped text, -1 if no key was found
    orig: the position of first key in original string, -1 if no key was found
    keys: list of (key,pos,orig) tuples with all valid hotkeys that were found
    text: the string stripped of all '&'s that precedes a valid hotkey char, if
          any. All '&&' are also replaced for '&'. Other '&'s, if any, are kept

    Examples: (showing only key, pos, orig, text as a tuple for clarity)
    hotkey(E&XIT)           => ('x', 1, 2, 'EXIT')
    hotkey(&Play D&&D)      => ('p', 0, 1, 'Play D&D')
    hotkey(Romeo & &Juliet) => ('j', 8, 9, 'Romeo & Juliet')
    hotkey(Trailing&)       => ('' ,-1,-1, 'Trailing&')
    hotkey(&Multiple&Keys)  => ('m', 0, 1, 'MultipleKeys') (also ('k', 8, 10))
    hotkey(M&&&M)           => ('m', 2, 4, 'M&M')
    """

    def remove_index(s, i):
        return s[:i] + s[i + 1 :]

    def remove_accents(text):
        from unicodedata import normalize, combining

        nfkd_form = normalize("NFKD", str(text))
        return "".join(c for c in nfkd_form if not combining(c))

    text = string
    keys = []
    shift = 0  # counts stripped '&'s, both '&<key>' and '&&'

    pos = text.find("&")
    while pos >= 0:
        char = text[pos + 1 : pos + 2]

        if char.isalpha() or char.isdigit():
            keys.append((remove_accents(char).lower(), pos, pos + shift + 1))
            text = remove_index(text, pos)  # Remove '&'
            shift += 1

        elif char == "&":
            text = remove_index(text, pos)
            shift += 1

        pos = text.find("&", pos + 1)  # Skip char

    if keys:
        key = keys[0][0]  # first key char
        pos = keys[0][1]  # first key position in stripped text
        orig = keys[0][2]  # first key position in original string
    else:
        key = ""
        pos = -1
        orig = -1

    return dict(key=key, pos=pos, orig=orig, keys=keys, text=text)


# Convenience shortcuts
def get_hotkey(string):
    return hotkey(string)["key"]


def strip_hotkey(string):
    return hotkey(string)["text"]


def hotkey_position(string):
    return hotkey(string)["pos"]


# Demo code for safety.safe, runs on game start.
# load_sounds()
# from safety import safe
# @safe(on_error = "Made it!")
# def raises_exception():
#   raise Exception, "Aaaaaargh!"
#
# print raises_exception()
