#! /usr/bin/env python3
# -*- coding: utf-8 -*-

import json
import math
import re
import time
import traceback
from decimal import *

from ikabot.config import *
from ikabot.helpers.botComm import *
from ikabot.helpers.getJson import getCity
from ikabot.helpers.gui import *
from ikabot.helpers.naval import *
from ikabot.helpers.pedirInfo import *
from ikabot.helpers.planRoutes import waitForArrival
from ikabot.helpers.process import set_child_mode
from ikabot.helpers.signals import setInfoSignal
from ikabot.helpers.varios import *
from ikabot.helpers.pedirInfo import getShipCapacity

getcontext().prec = 30


def choose_island(session):
    idsIslands = getIslandsIds(session)
    islands = []
    for idIsland in idsIslands:
        html = session.get(island_url + idIsland)
        island = getIsland(html)
        islands.append(island)

    if len(islands) == 1:
        return islands[0]

    islands.sort(key=lambda island: island["id"])

    longest_island_name_length = 0
    for island in islands:
        longest_island_name_length = max(
            len(island["name"]), longest_island_name_length
        )

    def pad(island_name):
        return " " * (longest_island_name_length - len(island_name)) + island_name

    print("In which island do you want to attack the barbarians?")
    print(" 0) Exit")
    for i, island in enumerate(islands):
        num = " " + str(i + 1) if i < 9 else str(i + 1)
        if island["barbarians"]["destroyed"] == 1:
            warn = "(currently destroyed)"
        else:
            warn = ""
        print(
            "{}) [{}:{}] {} ({}) : barbarians lv: {} ({}) {}".format(
                num,
                island["x"],
                island["y"],
                pad(island["name"]),
                materials_names[int(island["tradegood"])][0].upper(),
                island["barbarians"]["level"],
                island["barbarians"]["city"],
                warn,
            )
        )

    index = read(min=0, max=len(islands))
    if index == 0:
        return None
    else:
        return islands[index - 1]


def get_barbarians_lv(session, island, ship_capacity):
    params = {
        "view": "barbarianVillage",
        "destinationIslandId": island["id"],
        "oldBackgroundView": "city",
        "cityWorldviewScale": "1",
        "islandId": island["id"],
        "backgroundView": "island",
        "currentIslandId": island["id"],
        "actionRequest": actionRequest,
        "ajax": "1",
    }
    resp = session.post(params=params)
    resp = json.loads(resp, strict=False)

    level = int(math.ceil(resp[2][1]["js_islandBarbarianLevel"]["text"]))
    gold = int(resp[2][1]["js_islandBarbarianResourcegold"]["text"].replace(",", ""))

    resources = [0] * len(materials_names)
    for i in range(len(materials_names)):
        if i == 0:
            resources[i] = int(
                resp[2][1]["js_islandBarbarianResourceresource"]["text"].replace(
                    ",", ""
                )
            )
        else:
            resources[i] = int(
                resp[2][1]["js_islandBarbarianResourcetradegood{:d}".format(i)][
                    "text"
                ].replace(",", "")
            )

    html = resp[1][1][1]
    troops = re.findall(
        r'<div class="army \w*?">\s*<div class=".*?">(.*?)</div>\s*</div>\s*</td>\s*</tr>\s*<tr>\s*<td class="center">\s*(\d+)',
        html,
    )

    total_cargo = sum(resources)
    ships = math.ceil(Decimal(total_cargo) / Decimal(ship_capacity))

    info = {
        "island_id": island["id"],
        "level": level,
        "gold": gold,
        "resources": resources,
        "troops": troops,
        "ships": ships,
    }

    return info


def get_units(session, city):
    params = {
        "view": "cityMilitary",
        "activeTab": "tabUnits",
        "cityId": city["id"],
        "backgroundView": "city",
        "currentCityId": city["id"],
        "currentTab": "multiTab1",
        "actionRequest": actionRequest,
        "ajax": "1",
    }

    resp = session.post(params=params)
    resp = json.loads(resp, strict=False)
    html = resp[1][1][1]
    html = html.split('<div class="fleet')[0]

    unit_id_names = re.findall(
        r'<div class="army (.*?)">\s*<div class="tooltip">(.*?)<\/div>', html
    )
    unit_amounts = re.findall(r"<td>(.*?)\s*</td>", html)

    units = {}
    for i in range(len(unit_id_names)):
        amount = int(unit_amounts[i].replace(",", "").replace("-", "0"))
        unit_id = unit_id_names[i][0][1:]
        unit_name = unit_id_names[i][1]
        units[unit_id] = {}
        units[unit_id]["name"] = unit_name
        units[unit_id]["amount"] = amount

    return units


def plan_attack(session, city, babarians_info):
    total_units = get_units(session, city)

    if sum([total_units[unit_id]["amount"] for unit_id in total_units]) == 0:
        print("You don't have any troops in this city!")
        enter()
        return None

    plan = []
    total_ships = None
    last = False
    while True:

        banner()

        units_available = {}
        for unit_id in total_units:

            already_sent = sum(
                [p["units"][u] for p in plan for u in p["units"] if u == unit_id]
            )
            if already_sent < total_units[unit_id]["amount"]:
                units_available[unit_id] = {}
                units_available[unit_id]["amount"] = (
                    total_units[unit_id]["amount"] - already_sent
                )
                units_available[unit_id]["name"] = total_units[unit_id]["name"]

        if len(units_available) == 0:
            print("No more troops available to send")
            enter()
            break

        attack_round = {}
        attack_round["units"] = {}
        print("Which troops do you want to send?")
        for unit_id in units_available:
            unit_amount = units_available[unit_id]["amount"]
            unit_name = units_available[unit_id]["name"]
            amount_to_send = read(
                msg="{} (max: {}): ".format(
                    unit_name, addThousandSeparator(unit_amount)
                ),
                max=unit_amount,
                default=0,
            )
            if amount_to_send > 0:
                attack_round["units"][unit_id] = amount_to_send
        print("")

        attack_round["loot"] = last
        if last:
            attack_round["round"] = len(plan) + 1
        else:
            if len(plan) > 0:
                round_def = len(plan) + 1
                attack_round["round"] = read(
                    msg= "In which battle round do you want to send them? (min: 2, default: {:d}): ".format(round_def),
                    min=2,
                    default=round_def,
                )
            else:
                attack_round["round"] = 1
        print("")

        if last is False:
            if total_ships is None:
                total_ships = getTotalShips(session)
            max_ships = total_ships - sum([ar["ships"] for ar in plan])
            if max_ships > 0:
                attack_round["ships"] = read(
                    msg= "How many ships do you want to send in this round? (min: 0, max: {:d}): ".format(max_ships),
                    min=0,
                    max=max_ships,
                )
                print("")
            else:
                attack_round["ships"] = 0

        plan.append(attack_round)

        if last:
            break

        print("Do you want to send another round of troops? [y/N]")
        resp = read(values=["y", "Y", "n", "N"], default="n")
        if resp.lower() != "y":
            print("")
            print("Do you want to select the troops that will be used to collect the remaining resources? (they need to destroy the wall) [y/N]")
            resp = read(values=["y", "Y", "n", "N"], default="n")
            if resp.lower() != "y":
                break
            else:
                last = True

    plan.sort(key=lambda ar: ar["round"])
    return plan


def attackBarbarians(session, event, stdin_fd, predetermined_input):
    """
    Parameters
    ----------
    session : ikabot.web.session.Session
    event : multiprocessing.Event
    stdin_fd: int
    predetermined_input : multiprocessing.managers.SyncManager.list
    """
    sys.stdin = os.fdopen(stdin_fd)
    config.predetermined_input = predetermined_input
    try:
        banner()

        island = choose_island(session)
        if island is None:
            event.set()
            return
        ship_capacity, freighter_capacity = getShipCapacity(session)
        babarians_info = get_barbarians_lv(session, island, ship_capacity)

        banner()
        print("The barbarians have:")
        for name, amount in babarians_info["troops"]:
            print("{} units of {}".format(amount, name))
        print("")

        banner()
        print("From which city do you want to attack?")
        city = chooseCity(session)

        plan = plan_attack(session, city, babarians_info)
        if plan is None:
            event.set()
            return

        banner()
        print(
            "The barbarians in [{}:{}] will be attacked.".format(
                island["x"], island["y"]
            )
        )
        enter()

    except KeyboardInterrupt:
        event.set()
        return

    set_child_mode(session)
    event.set()

    info = "\nI attack the barbarians in [{}:{}]\n".format(island["x"], island["y"])
    setInfoSignal(session, info)
    try:
        do_it(session, island, city, babarians_info, plan, ship_capacity)
    except Exception as e:
        msg = "Error in:\n{}\nCause:\n{}".format(info, traceback.format_exc())
        sendToBot(session, msg)
    finally:
        session.logout()


def get_unit_data(session, city_id, unit_id):
    params_w = {
        "view": "unitdescription",
        "unitId": unit_id,
        "helpId": 9,
        "subHelpId": 0,
        "backgroundView": "city",
        "currentCityId": city_id,
        "templateView": "unitdescription",
        "actionRequest": actionRequest,
        "ajax": 1,
    }
    resp = session.post(params=params_w)
    resp = json.loads(resp, strict=False)
    html = resp[1][1][1]

    weight = re.search(
        r'<li class="weight fifthpos" title=".*?"><span\s*class="accesshint">\'.*?\': </span>(\d+)</li>',
        html,
    ).group(1)
    weight = int(weight)

    speed = re.search(
        r'<span class="textLabel">.*?</span>\s*(\d+)\s*<br/>',
        html,
    ).group(1)
    speed = int(speed)

    name = re.search(r'<div class="contentBox01h">\s*<h3 class="header">([^<]+)</h3>', html).group(1)
    name = str(name)

    return {"speed": speed, "weight": weight, "name": name}


def city_is_in_island(city, island):
    return city["x"] == island["x"] and city["y"] == island["y"]


def get_barbarians_info(session, island_id):
    query = {
        "view": "barbarianVillage",
        "destinationIslandId": island_id,
        "backgroundView": "island",
        "currentIslandId": island_id,
        "actionRequest": actionRequest,
        "ajax": 1,
    }
    resp = session.post(params=query)
    resp = json.loads(resp, strict=False)
    return resp


def wait_until_can_attack(session, city, island, travel_time=0):
    html = session.get(island_url + island["id"])
    island = getIsland(html)

    if (
        island["barbarians"]["underAttack"] == 0
        and island["barbarians"]["destroyed"] == 0
    ):
        # an attack might be on its way
        wait_for_arrival(session, city, island)
        html = session.get(island_url + island["id"])
        island = getIsland(html)

    if island["barbarians"]["underAttack"] == 1:
        # a battle is taking place
        attacks = get_current_attacks(session, city["id"], island["id"])
        attacks_fighting = filter_fighting(attacks)
        eventTimes = [attack["eventTime"] for attack in attacks_fighting]
        if len(eventTimes) > 0:
            wait_time = max(eventTimes)
            wait_time -= time.time()
            wait(wait_time + 5)
        wait_until_can_attack(session, city, island, travel_time)

    if island["barbarians"]["destroyed"] == 1:
        # the barbarians are destroyed and can't be attacked
        resp = get_barbarians_info(session, island["id"])
        if "barbarianCityCooldownTimer" in resp[2][1]:
            wait_time = resp[2][1]["barbarianCityCooldownTimer"]["countdown"]["enddate"]
            wait_time -= time.time()
            wait_time -= travel_time
            wait(wait_time + 5)
        wait_until_can_attack(session, city, island, travel_time)


def get_movements(session, city_id=None):
    if city_id is None:
        city_id = getCurrentCityId(session)
    query = {
        "view": "militaryAdvisor",
        "oldView": "updateGlobalData",
        "cityId": city_id,
        "backgroundView": "city",
        "currentCityId": city_id,
        "templateView": "militaryAdvisor",
        "actionRequest": actionRequest,
        "ajax": 1,
    }

    resp = session.post(params=query)
    resp = json.loads(resp, strict=False)
    movements = resp[1][1][2]["viewScriptParams"]["militaryAndFleetMovements"]

    return movements


def get_current_attacks(session, city_id, island_id):

    movements = get_movements(session, city_id)
    curr_attacks = []

    for movement in movements:
        if movement["event"]["mission"] != 13:
            continue
        if movement["target"]["islandId"] != int(island_id):
            continue
        if movement["event"]["isReturning"] != 0:
            continue
        if movement["origin"]["cityId"] == -1:
            continue

        curr_attacks.append(movement)

    return curr_attacks


def wait_for_arrival(session, city, island):
    attacks = get_current_attacks(session, city["id"], island["id"])
    attacks = filter_loading(attacks) + filter_traveling(attacks)
    eventTimes = [attack["eventTime"] for attack in attacks]

    if len(eventTimes) == 0:
        return

    wait_time = max(eventTimes)
    wait_time -= time.time()
    wait(wait_time + 5)

    wait_for_arrival(session, city, island)


def wait_for_round(session, city, island, travel_time, battle_start, round_number):
    if round_number == 1:
        wait_until_can_attack(session, city, island, travel_time)
    else:
        wait_time = battle_start + (round_number - 2) * 15 * 60
        wait_time -= time.time()
        wait_time -= travel_time
        wait(wait_time + 5)

        if battle_start < time.time():
            html = session.get(island_url + city["id"])
            island = getIsland(html)
            assert (
                island["barbarians"]["underAttack"] == 1
            ), "the battle ended before expected"


def calc_travel_time(city, island, speed):
    if city["x"] == island["x"] and city["y"] == island["y"]:
        return math.ceil(36000 / speed)
    else:
        return math.ceil(
            1200
            * math.sqrt(
                ((city["x"] - island["x"]) ** 2) + ((city["y"] - island["y"]) ** 2)
            )
        )


def filter_loading(attacks):
    return [attack for attack in attacks if attack["event"]["missionState"] == 1]


def filter_traveling(attacks,onlyCanAbort=True):
    return [
        attack
        for attack in attacks
        if attack["event"]["missionState"] == 2 and (onlyCanAbort is False or (onlyCanAbort is True and attack["event"]["canAbort"]))
    ]


def filter_fighting(attacks):
    return [
        attack
        for attack in attacks
        if attack["event"]["missionState"] == 2 and attack["event"]["canRetreat"]
    ]


def wait_until_attack_is_over(session, city, island):
    html = session.get(island_url + island["id"])
    island = getIsland(html)

    while island["barbarians"]["destroyed"] == 0:

        attacks = get_current_attacks(session, city["id"], island["id"])
        # the attack probably failed
        if len(attacks) == 0:
            return

        eventTimes = [attack["eventTime"] for attack in attacks]
        wait_time = min(eventTimes)
        wait_time -= time.time()
        wait(wait_time + 5)

        html = session.get(island_url + island["id"])
        island = getIsland(html)


def load_troops(
    session, city, island, attack_round, units_data, attack_data, ship_capacity, extra_cargo=0
):
    ships_needed = Decimal(extra_cargo) / Decimal(ship_capacity)
    speeds = []
    current_units = get_units(session, city)
    for unit_id in attack_round["units"]:
        amount_to_send = min(
            attack_round["units"][unit_id], current_units[unit_id]["amount"]
        )
        attack_data["cargo_army_{}".format(unit_id)] = amount_to_send

        if unit_id not in units_data:
            units_data[unit_id] = get_unit_data(session, city["id"], unit_id)

        speeds.append(units_data[unit_id]["speed"])

        if city_is_in_island(city, island) is False:
            weight = units_data[unit_id]["weight"]
            ships_needed += Decimal(amount_to_send * weight) / Decimal(ship_capacity)

    ships_needed = math.ceil(ships_needed)
    speed = min(speeds)
    travel_time = calc_travel_time(city, island, speed)
    return attack_data, ships_needed, travel_time


def loot(session, island, city, units_data, loot_round, ship_capacity):
    while True:

        attack_data = {
            "action": "transportOperations",
            "function": "attackBarbarianVillage",
            "actionRequest": actionRequest,
            "islandId": island["id"],
            "destinationCityId": 0,
            "cargo_army_304_upkeep": 3,
            "cargo_army_304": 0,
            "cargo_army_315_upkeep": 1,
            "cargo_army_315": 0,
            "cargo_army_302_upkeep": 4,
            "cargo_army_302": 0,
            "cargo_army_303_upkeep": 3,
            "cargo_army_303": 0,
            "cargo_army_312_upkeep": 15,
            "cargo_army_312": 0,
            "cargo_army_309_upkeep": 45,
            "cargo_army_309": 0,
            "cargo_army_307_upkeep": 15,
            "cargo_army_307": 0,
            "cargo_army_306_upkeep": 25,
            "cargo_army_306": 0,
            "cargo_army_305_upkeep": 30,
            "cargo_army_305": 0,
            "cargo_army_311_upkeep": 20,
            "cargo_army_311": 0,
            "cargo_army_310_upkeep": 10,
            "cargo_army_310": 0,
            "transporter": 0,
            "barbarianVillage": 1,
            "backgroundView": "island",
            "currentIslandId": island["id"],
            "templateView": "plunder",
            "ajax": 1,
        }

        # make sure we have ships on the port
        ships_available = waitForArrival(session)

        # if the barbarians are active again or all the resources were stolen, return
        html = session.get(island_url + island["id"])
        island = getIsland(html)
        destroyed = island["barbarians"]["destroyed"] == 1
        resources = get_barbarians_lv(session, island, ship_capacity)["resources"]
        if destroyed is False or sum(resources) == 0:
            return

        # if we already sent an attack and we still have ships on the port, it was the last one
        attacks = get_current_attacks(session, city["id"], island["id"])
        attacks = filter_loading(attacks) + filter_traveling(attacks)
        if len(attacks) > 0:
            return

        attack_data, ships_needed, travel_time = load_troops(
            session, city, island, loot_round, units_data, attack_data, ship_capacity, sum(resources)
        )
        attack_data["transporter"] = min(ships_available, ships_needed)

        # make sure we have time to send the attack
        time_left = None
        resp = get_barbarians_info(session, island["id"])
        if "barbarianCityCooldownTimer" in resp[2][1]:
            time_left = resp[2][1]["barbarianCityCooldownTimer"]["countdown"]["enddate"]
            time_left -= time.time()
        if time_left is not None and travel_time > time_left:
            return

        # send attack
        session.post(params=attack_data)


def do_it(session, island, city, babarians_info, plan, ship_capacity):

    units_data = {}

    battle_start = None

    for attack_round in plan:

        # this round is supposed to get the resources
        if attack_round["loot"]:
            break

        attack_data = {
            "action": "transportOperations",
            "function": "attackBarbarianVillage",
            "actionRequest": actionRequest,
            "islandId": island["id"],
            "destinationCityId": 0,
            "cargo_army_304_upkeep": 3,
            "cargo_army_304": 0,
            "cargo_army_315_upkeep": 1,
            "cargo_army_315": 0,
            "cargo_army_302_upkeep": 4,
            "cargo_army_302": 0,
            "cargo_army_303_upkeep": 3,
            "cargo_army_303": 0,
            "cargo_army_312_upkeep": 15,
            "cargo_army_312": 0,
            "cargo_army_309_upkeep": 45,
            "cargo_army_309": 0,
            "cargo_army_307_upkeep": 15,
            "cargo_army_307": 0,
            "cargo_army_306_upkeep": 25,
            "cargo_army_306": 0,
            "cargo_army_305_upkeep": 30,
            "cargo_army_305": 0,
            "cargo_army_311_upkeep": 20,
            "cargo_army_311": 0,
            "cargo_army_310_upkeep": 10,
            "cargo_army_310": 0,
            "transporter": 0,
            "barbarianVillage": 1,
            "backgroundView": "island",
            "currentIslandId": island["id"],
            "templateView": "plunder",
            "ajax": 1,
        }

        attack_data, ships_needed, travel_time = load_troops(
            session, city, island, attack_round, units_data, attack_data, ship_capacity
        )

        try:
            wait_for_round(
                session, city, island, travel_time, battle_start, attack_round["round"]
            )
        except AssertionError:
            # battle ended before expected
            break

        ships_available = waitForArrival(session)
        while ships_available < ships_needed:
            ships_available = waitForArrival(session)
        ships_available -= ships_needed

        # if the number of available troops changed, the POST request might not work as intended

        attack_data["transporter"] = min(
            babarians_info["ships"], attack_round["ships"], ships_available
        )

        # send new round
        session.post(params=attack_data)

        if attack_round["round"] == 1:
            battle_start = time.time() + travel_time

    wait_until_attack_is_over(session, city, island)

    last_round = plan[-1]
    if last_round["loot"]:
        loot(session, city, island, units_data, last_round, ship_capacity)
