from monopyly import *
from collections import defaultdict, Counter
from operator import itemgetter
from .cash_flow import CashFlow
from .colour_set import *


class Strategy(object):

    class Mode(object):
        ACTIVE_BUY = 1
        SELECTIVE_BUY = 2
        FUND_RAISE = 3
        PRE_BUILD = 4
        BUILD_HOUSE = 5
        MONEY_LOW = 6

    class DealStage(object):
        DEAL1 = 0
        DEAL2 = 1
        DEAL3 = 2

    def __init__(self, game_state, player):
        self.holder = player
        self.set = ColourSetProfile(game_state, self.holder)
        self.cash = CashFlow(game_state, self.holder)

        self.game_type = None
        self.ply_in_action = None

        self.mode = Strategy.Mode.ACTIVE_BUY
        self.eminent_domain = False

        self.sorted_targets = None
        self.set_to_build = None
        self.build_cost = None
        self.money_will_be_taken = 0

        self.show_log = False
        self.nc = defaultdict(float)
        self.edc = defaultdict(float)

        self.deal_stage = self.DealStage.DEAL1
        self.completed_deal_proposer = Counter()
        self.completed_deal_proposee = Counter()
        self.buy_acceptor = Counter()
        self.buy_rejector = Counter()
        self.sell_acceptor = Counter()
        self.sell_rejector = Counter()
        self.buy_candidates = None
        self.sell_candidates = None
        self.last_propose_buy = None
        self.last_propose_sell = None
        self.tx_deal_flag = 1
        self.rx_deal = None
        self.retry_buy = 0
        self.retry_sell = 0
        self.update(game_state)

    def update(self, game_state):
        self.set.update(game_state)
        self.set.generate_target_profile(game_state)
        self.cash.refresh(game_state)

    def display_log(self, turn_on):
        self.show_log = turn_on

    def decide_mode(self):
        # Plan A - build house
        self.set_to_build = None
        self.set.select_target()

        number_of_candidates = len(self.set.my_target)
        if number_of_candidates > 0:
            self.sorted_targets = sorted(self.set.my_target, key=itemgetter(ColourSetProfile.RETURN_ON_INVESTMENT), reverse=True)
            reserve_money = self.cash.elem[self.holder][CashFlow.ONE_ROLL]

            found, idx = False, 0
            for i in range(number_of_candidates):
                if self.holder.state.cash + self.cash.elem[self.holder][CashFlow.MORTGAGEABLE] > self.sorted_targets[i][ColourSetProfile.COST] + reserve_money:
                    found, idx = True, i
                    break
                else:
                    max_can_be_raised = self.set.money_to_be_raised_to_build_house(self.sorted_targets[i][ColourSetProfile.NAME])
                    if max_can_be_raised + self.holder.state.cash > self.sorted_targets[i][ColourSetProfile.COST] + reserve_money:
                        found, idx = True, i
                        break
            if found is True:
                self.set_to_build = self.sorted_targets[idx][ColourSetProfile.NAME]
                self.build_cost = self.sorted_targets[idx][ColourSetProfile.COST]
                self.mode = Strategy.Mode.BUILD_HOUSE
                if self.show_log:
                    Logger.log("~mode {0}, build: {1}, candidates: {2}".format(self.mode, self.set_to_build, self.set.my_target))
                return

            found, idx = False, 0
            for i in range(number_of_candidates):
                unmortgage_cost = self.sorted_targets[i][ColourSetProfile.UNMORTGAGE_COST]
                if unmortgage_cost > 0 and self.holder.state.cash > (unmortgage_cost + reserve_money):
                    found, idx = True, i
                    break
            if found is True:
                self.set_to_build = self.sorted_targets[idx][ColourSetProfile.NAME]
                self.build_cost = self.sorted_targets[idx][ColourSetProfile.COST]
                self.mode = Strategy.Mode.PRE_BUILD
                if self.show_log:
                    Logger.log("~mode {0}, pre_build: {1}, candidates: {2}".format(self.mode, self.set_to_build, self.set.my_target))
                return

            if (self.holder.state.cash + self.cash.elem[self.holder][CashFlow.MORTGAGEABLE]) > self.cash.elem[self.holder][CashFlow.MAX_COST]:
                self.mode = Strategy.Mode.FUND_RAISE
            else:
                self.mode = Strategy.Mode.MONEY_LOW
            return

        # no house to build, decide what to do next
        if (self.holder.state.cash + self.cash.elem[self.holder][CashFlow.MORTGAGEABLE]) > 2 * self.cash.elem[self.holder][CashFlow.MAX_COST]:
            self.mode = Strategy.Mode.ACTIVE_BUY
        elif (self.holder.state.cash + self.cash.elem[self.holder][CashFlow.MORTGAGEABLE]) > self.cash.elem[self.holder][CashFlow.MAX_COST]:
            self.mode = Strategy.Mode.SELECTIVE_BUY
        else:
            self.mode = Strategy.Mode.MONEY_LOW
        if self.show_log:
            Logger.log("~mode {0}, build target: {1}, candidates: {2}".format(self.mode, self.set_to_build, self.set.my_target))

    #----- make deals -----
    def deal_init(self, game_state):
        self.deal_stage = self.DealStage.DEAL1
        self.retry_buy = 0
        self.retry_sell = 0
        self.tx_deal_flag = 0

    def propose_deal(self, game_state):
        if self.mode is Strategy.Mode.FUND_RAISE or self.mode is Strategy.Mode.MONEY_LOW:
            self.tx_deal_flag = 1
        elif self.mode is Strategy.Mode.ACTIVE_BUY:
            self.tx_deal_flag = 0
        else:
            return None

        if self.tx_deal_flag == 0:
            buy_list, price, seller = [], 0, None
            if self.deal_stage is self.DealStage.DEAL1:
                self.retry_buy = 0
                self.buy_candidates = self.set.buy_candidates()
                property = self.buy_candidates.pop()
                while property is not None:
                    if self._is_bad_seller(property.owner) is False:
                        price = 2 * property.price + 10
                        if property.is_mortgaged:
                            price = property.price
                        buy_list.append(property)
                        seller = property.owner
                        break
                    property = self.buy_candidates.pop()

            elif self.deal_stage is self.DealStage.DEAL2 or self.deal_stage is self.DealStage.DEAL3:
                if self.retry_buy > 0:
                    self.retry_buy = 0
                    buy_list, price, seller = self.last_propose_buy
                    price += 50
                else:
                    property = self.buy_candidates.pop()
                    while property is not None:
                        if self._is_bad_seller(property.owner) is False:
                            price = 2 * property.price + 10
                            if property.is_mortgaged:
                                price = property.price
                            buy_list.append(property)
                            seller = property.owner
                        property = self.buy_candidates.pop()

            self.deal_stage = (self.deal_stage + 1) % 3
            if len(buy_list) > 0:
                self.last_propose_buy = buy_list, price, seller
                self.tx_deal_flag = 0
                if self.show_log:
                    Logger.log("~propose_deal: to buy {0} at {1} from {2}".format(buy_list, price, seller))
                return DealProposal(
                    properties_wanted=buy_list,
                    maximum_cash_offered=price,
                    propose_to_player=seller)

        elif self.tx_deal_flag == 1:
            sell_list, price, buyer = [], 0, None
            if self.deal_stage is self.DealStage.DEAL1:
                self.retry_sell = 0
                self.sell_candidates = self.set.sell_candidates(game_state)
                candidate = self.sell_candidates.pop()
                while candidate is not None:
                    property, buyer = candidate
                    if self.show_log:
                        Logger.log("~propose_deal sell candidate: {0},  {1}".format(property, buyer))

                    if self._is_bad_buyer(buyer) is False and property.owner is self.holder:
                        price = property.price - 2
                        sell_list.append(property)
                        break
                    candidate = self.sell_candidates.pop()
            elif self.deal_stage is self.DealStage.DEAL2 or self.deal_stage is self.DealStage.DEAL3:
                if self.retry_sell > 0:
                    self.retry_sell = 0
                    sell_list, price, buyer = self.last_propose_sell
                    price -= 5
                else:
                    candidate = self.sell_candidates.pop()
                    while candidate is not None:
                        property, buyer = candidate
                        if self._is_bad_buyer(buyer) is False and property.owner is self.holder:
                            price = property.price - 2
                            sell_list.append(property)
                            break
                        candidate = self.sell_candidates.pop()

            self.deal_stage = (self.deal_stage + 1) % 3
            if len(sell_list) > 0:
                self.last_propose_sell = sell_list, price, buyer
                self.tx_deal_flag = 1
                if self.show_log:
                    Logger.log("~propose_deal: to sell {0} at {1} to {2}".format(sell_list, price, buyer))
                return DealProposal(
                    propose_to_player=buyer,
                    properties_offered=sell_list,
                    minimum_cash_wanted=price)
        else:
            return None

    def deal_proposed(self, game_state, deal_proposal):
        self.rx_deal = True
        if self.show_log:
            Logger.log("~deal_proposed: {0}, {1}".format(self.mode, self.ply_in_action))

        #-- sell --
        if len(deal_proposal.properties_wanted) == 1 and len(deal_proposal.properties_offered) == 0:
            if self.mode is not Strategy.Mode.FUND_RAISE or self.mode is not Strategy.Mode.MONEY_LOW:
                return DealResponse(DealResponse.Action.REJECT)

            self.update(game_state)

            property = deal_proposal.properties_wanted[0]
            enum = property.property_set.set_enum
            tag = self.set.profile[enum].tag
            factor = self.set.profile[enum].factor
            owners = self.set.profile[enum].owners
            number_of_unowned_properties = self.set.profile[enum].number_of_unowned_properties
            proposer = deal_proposal.proposed_by_player
            if self.show_log:
                Logger.log("~ Do I sell property {0} to {1}?  {2} {3} {4}, is_mortgaged: {5}, owners: {6}".format(property, proposer, enum, tag, factor, property.is_mortgaged, owners))

            if tag is ColourSet.MY_MIX:
                if proposer not in owners or len(owners) > 2:
                    price = property.price
                    if property.is_mortgaged:
                        price -= 5
                    return DealResponse(action=DealResponse.Action.ACCEPT, minimum_cash_wanted=price)

        #-- buy --
        if len(deal_proposal.properties_wanted) == 0 and len(deal_proposal.properties_offered) == 1 and self.mode is Strategy.Mode.ACTIVE_BUY:
            self.update(game_state)

            property = deal_proposal.properties_offered[0]
            enum = property.property_set.set_enum
            tag = self.set.profile[enum].tag
            factor = self.set.profile[enum].factor
            proposer = deal_proposal.proposed_by_player
            if self.show_log:
                Logger.log("~ Do I buy property {0} from {1}?  {2} {3} {4}, is_mortgaged: {5} -- cash {6} 1R {7}".format(property, proposer, enum, tag, factor, property.is_mortgaged, self.holder.state.cash, self.cash.elem[self.holder][CashFlow.ONE_ROLL]))

            if tag is ColourSet.MY_HALF:
                price = property.price + 10
                if factor == 95:
                    price = 2 * property.price + 5
                if property.is_mortgaged:
                    price = int(price/2) + 5
                return DealResponse(action=DealResponse.Action.ACCEPT, maximum_cash_offered=price)

        #-- reject --
        return DealResponse(DealResponse.Action.REJECT)

    def deal_completed(self, deal_result):
        self.completed_deal_proposer[deal_result.proposer] += 1
        self.completed_deal_proposee[deal_result.proposee] += 1

    def _is_bad_seller(self, seller):
        return self.buy_acceptor[seller] < 2 and self.buy_rejector[seller] > 15

    def _is_bad_buyer(self, seller):
        return self.sell_acceptor[seller] < 2 and self.sell_rejector[seller] > 15

    def deal_result(self, deal_info):
        if self.rx_deal is True:
            self.rx_deal = False

        elif self.tx_deal_flag == 0:
            buy_list, price, seller = self.last_propose_buy
            if deal_info is PlayerAIBase.DealInfo.SUCCEEDED:
                self.buy_acceptor[seller] += 1
            elif deal_info is PlayerAIBase.DealInfo.DEAL_REJECTED:
                self.buy_rejector[seller] += 1
            elif deal_info is PlayerAIBase.DealInfo.OFFERED_TOO_LITTLE_MONEY:
                self.retry_buy += 1
        elif self.tx_deal_flag == 1:
            sell_list, price, buyer = self.last_propose_sell
            if deal_info is PlayerAIBase.DealInfo.SUCCEEDED:
                self.sell_acceptor[buyer] += 1
            elif deal_info is PlayerAIBase.DealInfo.DEAL_REJECTED:
                self.sell_rejector[buyer] += 1
            elif deal_info is PlayerAIBase.DealInfo.ASKED_FOR_TOO_MUCH_MONEY:
                self.retry_sell += 1

    #----- build houses -----
    def unmortgage_properties(self, game_state):
        to_be_unmortgaged = []
        cost = 0
        if self.mode is Strategy.Mode.BUILD_HOUSE or self.mode is Strategy.Mode.PRE_BUILD:
            for p in self.set.profile[self.set_to_build].property_set.properties:
                if p.owner is self.holder and p.is_mortgaged:
                    to_be_unmortgaged.append(p)
                    cost += int(1.1 * p.mortgage_value)
        elif self.holder.state.cash > 3 * self.cash.elem[self.holder][CashFlow.MAX_COST]:
            candidates = self.set.unmortgage_candidates()
            candidate = candidates.pop()
            if candidate is not None:
                properties, money_required = candidate
                if (self.holder.state.cash - money_required) > self.cash.elem[self.holder][CashFlow.MAX_COST]:
                    to_be_unmortgaged = properties
                    cost = money_required

        if self.show_log and len(to_be_unmortgaged) > 0:
            Logger.log("~unmortgage_properties_list: {0}, {1}".format(cost, to_be_unmortgaged))
        return to_be_unmortgaged

    def build_houses(self, game_state):
        to_be_built = []

        if self.mode is Strategy.Mode.BUILD_HOUSE:
            for p in self.set.profile[self.set_to_build].property_set.properties:
                to_be_built.append((p, 1))

        if self.show_log and len(to_be_built) > 0:
            Logger.log("~build_houses_list: {0}".format(to_be_built))
        return to_be_built

    def mortgage_properties(self, game_state):
        if self.show_log:
            Logger.log("~mortgage_properties: {0}, {1}".format(self.mode, self.money_will_be_taken))

        to_be_mortgaged, money_raised = [], 0
        if self.holder.state.cash < self.money_will_be_taken:
            deficit = self.money_will_be_taken - self.holder.state.cash
            candidates = self.set.mortgage_candidates()
            while True:
                property = candidates.pop()
                if property is None:
                    break
                to_be_mortgaged.append(property)
                money_raised += property.mortgage_value
                if money_raised > deficit:
                    break

        if self.show_log and len(to_be_mortgaged) > 0:
            Logger.log("~ mortgage_properties_list: {0}, {1}".format(money_raised, to_be_mortgaged))
        return to_be_mortgaged

    #----- take it or leave it -----
    def landed_on_unowned_property(self, game_state, property):
        tag = self.set.profile[property.property_set.set_enum].tag
        factor = self.set.profile[property.property_set.set_enum].factor
        if self.show_log:
            Logger.log("~landed_on_unowned: {0} ({1} {2})".format(property, tag, factor))

        if property.property_set.set_enum is PropertySet.UTILITY:
            return PlayerAIBase.Action.DO_NOT_BUY

        if (self.holder.state.cash + self.cash.elem[self.holder][CashFlow.MORTGAGEABLE]) < (self.cash.elem[self.holder][CashFlow.ONE_ROLL] + property.price):
            return PlayerAIBase.Action.DO_NOT_BUY

        if tag is ColourSet.MY_HALF or tag is ColourSet.EMPTY or tag is ColourSet.OTHERS_HALF:
            return PlayerAIBase.Action.BUY

        return PlayerAIBase.Action.DO_NOT_BUY

    def property_offered_for_auction(self, game_state, property):
        tag = self.set.profile[property.property_set.set_enum].tag
        factor = self.set.profile[property.property_set.set_enum].factor

        if not self.eminent_domain:
            offer = property.price

            if self.mode is Strategy.Mode.MONEY_LOW or property.property_set.set_enum is PropertySet.UTILITY:
                return offer

            if (self.holder.state.cash + self.cash.elem[self.holder][CashFlow.MORTGAGEABLE]) < (self.cash.elem[self.holder][CashFlow.ONE_ROLL] + offer):
                return offer

            if self.mode is not Strategy.Mode.ACTIVE_BUY and property.property_set.set_enum is PropertySet.STATION:
                return offer

            coefficient = 0
            for ply in game_state.players:
                if coefficient < self.nc[ply]:
                    coefficient = self.nc[ply]
            if coefficient > 3.0:
                coefficient = 3.0

            if tag is ColourSet.MY_HALF:
                if factor == 95:
                    offer = int((coefficient + 3) * property.price) + 5
                else:
                    offer = int((coefficient + 2) * property.price) + 5
            elif tag is ColourSet.EMPTY:
                offer = int((coefficient + 1.5) * property.price) + 5
            elif tag is ColourSet.OTHERS_HALF:
                if factor == 45:
                    offer = int((coefficient + 2) * property.price) + 5
            return offer

        else:
            #------ auction algorithm for ED ------
            self.update(game_state)

            build_cost = 0
            self.set.select_target()
            if len(self.set.my_target) > 0:
                self.sorted_targets = sorted(self.set.my_target, key=itemgetter(ColourSetProfile.COST), reverse=False)
                build_cost = self.sorted_targets[0][ColourSetProfile.COST]

            coefficient = 0
            for ply in game_state.players:
                if coefficient < self.edc[ply]:
                    coefficient = self.edc[ply]
            if coefficient > 5.0:
                coefficient = 5.0

            offer = 2 * property.price

            if self.show_log:
                Logger.log("~auction_ED: cash {0} build_cost {1}, ({2}, {3}) {4}, {5}, {6}".format(self.holder.state.cash, build_cost, property, property.property_set.set_enum, tag, factor, coefficient))

            if (self.holder.state.cash + self.cash.elem[self.holder][CashFlow.MORTGAGEABLE]) < (self.cash.elem[self.holder][CashFlow.ONE_ROLL] + build_cost):
                return offer

            if property.property_set.set_enum is PropertySet.UTILITY or property.property_set.set_enum is PropertySet.STATION:
                return offer

            if property.property_set.set_enum is PropertySet.BROWN or property.property_set.set_enum is PropertySet.LIGHT_BLUE:
                if factor == 95:
                    offer = int(coefficient + 3) * property.price + 1
                return offer

            if tag is ColourSet.MY_HALF:
                if factor == 95:
                    offer = int(coefficient + 3) * property.price + 1
                else:
                    offer = int(coefficient + 2) * property.price + 1
            elif tag is ColourSet.EMPTY:
                offer = int(coefficient + 1) * property.price + 1
            elif tag is ColourSet.OTHERS_HALF:
                if factor == 45:
                    offer = 3 * property.price
            return offer

    def auction_result(self, status, property, player, amount_paid):
        value = amount_paid/property.price
        if not self.eminent_domain:
            if value > self.nc[player]:
                self.nc[player] = value
        else:
            if value > self.edc[player]:
                self.edc[player] = value

        if self.show_log:
            Logger.log("~auction_result: is_eminent {0}, {1} or {2}, {3}, {4}, {5}".format(self.eminent_domain, self.nc[player], self.edc[player], property, player, amount_paid))

    def sell_houses(self, game_state):
        to_be_sold = []

        cash_flow = self.cash.elem[self.holder]
        deficit = self.money_will_be_taken - self.holder.state.cash - cash_flow[CashFlow.MORTGAGEABLE]
        if deficit > 0:
            if self.show_log:
                Logger.log("~sell_houses_check_1: deficit {0}, money_taken {1}, cash {2}, mtg1 {3}, sell {4}, mtg2 {5}".format(
                    deficit, self.money_will_be_taken, self.holder.state.cash, self.cash.elem[self.holder][CashFlow.MORTGAGEABLE],
                    self.cash.elem[self.holder][CashFlow.SELLABLE], self.cash.elem[self.holder][CashFlow.MORTGAGEABLE2]))

            self.update(game_state)
            cash_flow = self.cash.elem[self.holder]
            deficit = self.money_will_be_taken - self.holder.state.cash - cash_flow[CashFlow.MORTGAGEABLE]

            if self.show_log:
                Logger.log("~sell_houses_check_2: deficit {0}, money_taken {1}, cash {2}, mtg1 {3}, sell {4}, mtg2 {5}".format(
                    deficit, self.money_will_be_taken, self.holder.state.cash, self.cash.elem[self.holder][CashFlow.MORTGAGEABLE],
                    self.cash.elem[self.holder][CashFlow.SELLABLE], self.cash.elem[self.holder][CashFlow.MORTGAGEABLE2]))
            if deficit < 0:
                return

            money_raised = 0
            candidates = self.set.sell_house_candidates()
            candidate = candidates.pop()
            while deficit > money_raised and candidate is not None:
                properties, number_of_houses, house_price = candidate
                number_of_houses_to_be_sold = 1
                while deficit > money_raised and number_of_houses_to_be_sold < number_of_houses:
                    number_of_houses_to_be_sold += 1
                    money_raised += int(house_price/2)
                to_be_sold += [(p, number_of_houses_to_be_sold) for p in properties]
                candidate = candidates.pop()

            if self.show_log:
                Logger.log("~sell_houses_list: {0}, {1}".format(money_raised, to_be_sold))
            if len(to_be_sold) > 0:
                self.update(game_state)

        return to_be_sold

    def show(self, game_state):
        if self.show_log:
            self.cash.show(game_state)
            self.set.show(game_state)

    def set_eminent_domain(self, game_state):
        self.eminent_domain = True
        self.update(game_state)
        self.show(game_state)
        if self.show_log:
            Logger.log("~set_eminent_domain: {0}".format(self.eminent_domain))

    def set_money_will_be_taken(self, amount):
        self.money_will_be_taken = amount
        if self.show_log:
            Logger.log("~money_will_be_taken: {0}".format(amount))

    def get_out_of_jail(self, game_state):
        if self.show_log:
            Logger.log("~get_out_of_jail: card {0}".format(self.holder.state.number_of_get_out_of_jail_free_cards))

        running_cost = self.cash.elem[self.holder][CashFlow.COST]
        if running_cost > 1140:
            return PlayerAIBase.Action.STAY_IN_JAIL
        elif self.holder.state.number_of_get_out_of_jail_free_cards >= 1:
            return PlayerAIBase.Action.PLAY_GET_OUT_OF_JAIL_FREE_CARD
        else:
            return PlayerAIBase.Action.BUY_WAY_OUT_OF_JAIL