#include "bionics.h"
#include "character.h"
#include "flag.h"
#include "itype.h"
#include "make_static.h"
#include "map.h"
#include "memorial_logger.h"
#include "mutation.h"
#include "output.h"
#include "weakpoint.h"

static const bionic_id bio_ads( "bio_ads" );

static const json_character_flag json_flag_SEESLEEP( "SEESLEEP" );

bool Character::can_interface_armor() const
{
    bool okay = std::any_of( my_bionics->begin(), my_bionics->end(),
    []( const bionic & b ) {
        return b.powered && b.info().has_flag( STATIC( json_character_flag( "BIONIC_ARMOR_INTERFACE" ) ) );
    } );
    return okay;
}

resistances Character::mutation_armor( bodypart_id bp ) const
{
    resistances res;
    for( const trait_id &iter : get_mutations() ) {
        res += iter->damage_resistance( bp );
    }

    return res;
}

float Character::mutation_armor( bodypart_id bp, damage_type dt ) const
{
    return mutation_armor( bp ).type_resist( dt );
}

float Character::mutation_armor( bodypart_id bp, const damage_unit &du ) const
{
    return mutation_armor( bp ).get_effective_resist( du );
}

int Character::get_armor_bash( bodypart_id bp ) const
{
    return get_armor_bash_base( bp ) + armor_bash_bonus;
}

int Character::get_armor_cut( bodypart_id bp ) const
{
    return get_armor_cut_base( bp ) + armor_cut_bonus;
}

int Character::get_armor_bullet( bodypart_id bp ) const
{
    return get_armor_bullet_base( bp ) + armor_bullet_bonus;
}

// TODO: Reduce duplication with below function
int Character::get_armor_type( damage_type dt, bodypart_id bp ) const
{
    switch( dt ) {
        case damage_type::PURE:
        case damage_type::BIOLOGICAL:
            return 0;
        case damage_type::BASH:
            return get_armor_bash( bp );
        case damage_type::CUT:
            return get_armor_cut( bp );
        case damage_type::STAB:
            return get_armor_cut( bp ) * 0.8f;
        case damage_type::BULLET:
            return get_armor_bullet( bp );
        case damage_type::ACID:
        case damage_type::HEAT:
        case damage_type::COLD:
        case damage_type::ELECTRIC: {
            return worn.damage_resist( dt, bp ) + mutation_armor( bp, dt ) + bp->damage_resistance( dt );
        }
        case damage_type::NONE:
        case damage_type::NUM:
            // Let it error below
            break;
    }

    debugmsg( "Invalid damage type: %d", dt );
    return 0;
}

std::map<bodypart_id, int> Character::get_all_armor_type( damage_type dt,
        const std::map<bodypart_id, std::vector<const item *>> &clothing_map ) const
{
    std::map<bodypart_id, int> ret;
    for( const bodypart_id &bp : get_all_body_parts() ) {
        ret.emplace( bp, 0 );
    }

    for( std::pair<const bodypart_id, int> &per_bp : ret ) {
        const bodypart_id &bp = per_bp.first;
        switch( dt ) {
            case damage_type::PURE:
            case damage_type::BIOLOGICAL:
                // Characters cannot resist this
                return ret;
            /* BASH, CUT, STAB, and BULLET don't benefit from the clothing_map optimization */
            // TODO: Fix that
            case damage_type::BASH:
                per_bp.second += get_armor_bash( bp );
                break;
            case damage_type::CUT:
                per_bp.second += get_armor_cut( bp );
                break;
            case damage_type::STAB:
                per_bp.second += get_armor_cut( bp ) * 0.8f;
                break;
            case damage_type::BULLET:
                per_bp.second += get_armor_bullet( bp );
                break;
            case damage_type::ACID:
            case damage_type::HEAT:
            case damage_type::COLD:
            case damage_type::ELECTRIC: {
                for( const item *it : clothing_map.at( bp ) ) {
                    per_bp.second += it->damage_resist( dt, false, bp );
                }

                per_bp.second += mutation_armor( bp, dt );
                per_bp.second += bp->damage_resistance( dt );
                break;
            }
            case damage_type::NONE:
            case damage_type::NUM:
                debugmsg( "Invalid damage type: %d", dt );
                return ret;
        }
    }

    return ret;
}

int Character::get_armor_bash_base( bodypart_id bp ) const
{
    float ret = worn.damage_resist( damage_type::BASH, bp );
    for( const bionic_id &bid : get_bionics() ) {
        const auto bash_prot = bid->bash_protec.find( bp.id() );
        if( bash_prot != bid->bash_protec.end() ) {
            ret += bash_prot->second;
        }
    }

    ret += mutation_armor( bp, damage_type::BASH );
    ret += bp->damage_resistance( damage_type::BASH );
    return ret;
}

int Character::get_armor_cut_base( bodypart_id bp ) const
{
    float ret = worn.damage_resist( damage_type::CUT, bp );

    for( const bionic_id &bid : get_bionics() ) {
        const auto cut_prot = bid->cut_protec.find( bp.id() );
        if( cut_prot != bid->cut_protec.end() ) {
            ret += cut_prot->second;
        }
    }

    ret += mutation_armor( bp, damage_type::CUT );
    ret += bp->damage_resistance( damage_type::CUT );
    return ret;
}

int Character::get_armor_bullet_base( bodypart_id bp ) const
{
    float ret = worn.damage_resist( damage_type::BULLET, bp );

    for( const bionic_id &bid : get_bionics() ) {
        const auto bullet_prot = bid->bullet_protec.find( bp.id() );
        if( bullet_prot != bid->bullet_protec.end() ) {
            ret += bullet_prot->second;
        }
    }

    ret += mutation_armor( bp, damage_type::BULLET );
    ret += bp->damage_resistance( damage_type::BULLET );
    return ret;
}

int Character::get_armor_acid( bodypart_id bp ) const
{
    return get_armor_type( damage_type::ACID, bp );
}

int Character::get_env_resist( bodypart_id bp ) const
{
    float ret = worn.get_env_resist( bp );

    for( const bionic_id &bid : get_bionics() ) {
        const auto EP = bid->env_protec.find( bp.id() );
        if( EP != bid->env_protec.end() ) {
            ret += EP->second;
        }
    }

    if( bp == body_part_eyes && has_flag( json_flag_SEESLEEP ) ) {
        ret += 8;
    }
    return ret;
}

std::map<bodypart_id, int> Character::get_armor_fire( const
        std::map<bodypart_id, std::vector<const item *>> &clothing_map ) const
{
    return get_all_armor_type( damage_type::HEAT, clothing_map );
}

// adjusts damage unit depending on type by enchantments.
// the ITEM_ enchantments only affect the damage resistance for that one item, while the others affect all of them
static void armor_enchantment_adjust( Character &guy, damage_unit &du )
{
    switch( du.type ) {
        case damage_type::ACID:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_ACID );
            break;
        case damage_type::BASH:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_BASH );
            break;
        case damage_type::BIOLOGICAL:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_BIO );
            break;
        case damage_type::COLD:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_COLD );
            break;
        case damage_type::CUT:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_CUT );
            break;
        case damage_type::ELECTRIC:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_ELEC );
            break;
        case damage_type::HEAT:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_HEAT );
            break;
        case damage_type::STAB:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_STAB );
            break;
        case damage_type::BULLET:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_BULLET );
            break;
        default:
            return;
    }
    du.amount = std::max( 0.0f, du.amount );
}

void destroyed_armor_msg( Character &who, const std::string &pre_damage_name )
{
    if( who.is_avatar() ) {
        get_memorial().add(
            //~ %s is armor name
            pgettext( "memorial_male", "Worn %s was completely destroyed." ),
            pgettext( "memorial_female", "Worn %s was completely destroyed." ),
            pre_damage_name );
    }
    who.add_msg_player_or_npc( m_bad, _( "Your %s is completely destroyed!" ),
                               _( "<npcname>'s %s is completely destroyed!" ),
                               pre_damage_name );
}

void post_absorbed_damage_enchantment_adjust( Character &guy, damage_unit &du )
{
    switch( du.type ) {
        case damage_type::ACID:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::EXTRA_ACID );
            break;
        case damage_type::BASH:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::EXTRA_BASH );
            break;
        case damage_type::BIOLOGICAL:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::EXTRA_BIO );
            break;
        case damage_type::COLD:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::EXTRA_COLD );
            break;
        case damage_type::CUT:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::EXTRA_CUT );
            break;
        case damage_type::ELECTRIC:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::EXTRA_ELEC );
            break;
        case damage_type::HEAT:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::EXTRA_HEAT );
            break;
        case damage_type::STAB:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::EXTRA_STAB );
            break;
        case damage_type::BULLET:
            du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::EXTRA_BULLET );
            break;
        default:
            return;
    }
    du.amount = std::max( 0.0f, du.amount );
}

const weakpoint *Character::absorb_hit( const weakpoint_attack &, const bodypart_id &bp,
                                        damage_instance &dam )
{
    std::list<item> worn_remains;
    bool armor_destroyed = false;

    for( damage_unit &elem : dam.damage_units ) {
        if( elem.amount < 0 ) {
            // Prevents 0 damage hits (like from hallucinations) from ripping armor
            elem.amount = 0;
            continue;
        }

        // The bio_ads CBM absorbs damage before hitting armor
        if( has_active_bionic( bio_ads ) ) {
            if( elem.amount > 0 && get_power_level() > 24_kJ ) {
                // ADS requires 25 kJ to trigger
                // Assuming it absorbs damage at 10% efficiency, it can absorb at most 2500 J of energy
                // Taking muzzle energy of 5.56mm ammo as a reference, 1936 J is equal to 44 damage
                // Having this in mind, let's make 2500 J equal to 50 damage, which will be the maximum damage ADS can absorb
                // costs energy equal to the 10x damage^2, before it is reduced, regardless of how much it is reduced
                // a 50 damage attack would incur the whole 25kJ. A 12 damage attack would cost 1440j
                const int max_absorption = 50;
                units::energy power_cost = units::from_joule( std::max( -25000.0f,
                                           elem.amount * elem.amount * -10 ) );

                // If damage is higher than maximum absorption capability, lower the damage by a flat amount of this capability
                // Otherwise, divide the damage by X times, depending on damage type
                if( elem.type == damage_type::BASH ) {
                    elem.amount = elem.amount > max_absorption ? elem.amount - max_absorption : elem.amount / 2;
                } else if( elem.type == damage_type::CUT ) {
                    elem.amount = elem.amount > max_absorption ? elem.amount - max_absorption : elem.amount / 3;
                } else if( elem.type == damage_type::STAB || elem.type == damage_type::BULLET ) {
                    elem.amount = elem.amount > max_absorption ? elem.amount - max_absorption : elem.amount / 4;
                }
                mod_power_level( power_cost );
                add_msg_if_player( m_good,
                                   _( "The defensive forcefield surrounding your body ripples as it reduces velocity of incoming attack." ) );
            }
            if( elem.amount < 0 ) {
                elem.amount = 0;
            }
        }

        armor_enchantment_adjust( *this, elem );

        worn.absorb_damage( *this, elem, bp, worn_remains, armor_destroyed );

        passive_absorb_hit( bp, elem );

        post_absorbed_damage_enchantment_adjust( *this, elem );
        elem.amount = std::max( elem.amount, 0.0f );
    }
    map &here = get_map();
    for( item &remain : worn_remains ) {
        here.add_item_or_charges( pos(), remain );
    }
    if( armor_destroyed ) {
        drop_invalid_inventory();
    }
    return nullptr;
}

bool Character::armor_absorb( damage_unit &du, item &armor, const bodypart_id &bp,
                              const sub_bodypart_id &sbp, int roll )
{
    item::cover_type ctype = item::get_cover_type( du.type );

    // if the armor location has ablative armor apply that first
    if( armor.is_ablative() ) {
        ablative_armor_absorb( du, armor, sbp, roll );
    }

    // if the core armor is missed then exit
    if( roll > armor.get_coverage( sbp, ctype ) ) {
        return false;
    }

    // reduce the damage
    // -1 is passed as roll so that each material is rolled individually
    armor.mitigate_damage( du, sbp, -1 );

    // check if the armor was damaged
    item::armor_status damaged = armor.damage_armor_durability( du, bp );

    // describe what happened if the armor took damage
    if( damaged == item::armor_status::DAMAGED || damaged == item::armor_status::DESTROYED ) {
        describe_damage( du, armor );
    }
    return damaged == item::armor_status::DESTROYED;
}

bool Character::armor_absorb( damage_unit &du, item &armor, const bodypart_id &bp, int roll ) const
{
    item::cover_type ctype = item::get_cover_type( du.type );

    if( roll > armor.get_coverage( bp, ctype ) ) {
        return false;
    }

    // reduce the damage
    // -1 is passed as roll so that each material is rolled individually
    armor.mitigate_damage( du, bp, -1 );

    // check if the armor was damaged
    item::armor_status damaged = armor.damage_armor_durability( du, bp );

    // describe what happened if the armor took damage
    if( damaged == item::armor_status::DAMAGED || damaged == item::armor_status::DESTROYED ) {
        describe_damage( du, armor );
    }
    return damaged == item::armor_status::DESTROYED;
}

bool Character::ablative_armor_absorb( damage_unit &du, item &armor, const sub_bodypart_id &bp,
                                       int roll )
{
    item::cover_type ctype = item::get_cover_type( du.type );

    for( item_pocket *const pocket : armor.get_all_contained_pockets() ) {
        // if the pocket is ablative and not empty we should use its values
        if( pocket->get_pocket_data()->ablative && !pocket->empty() ) {
            // get the contained plate
            item &ablative_armor = pocket->front();

            float coverage = ablative_armor.get_coverage( bp, ctype );

            // if the attack hits this plate
            if( roll < coverage ) {
                damage_unit pre_mitigation = du;

                // mitigate the actual damage instance
                ablative_armor.mitigate_damage( du );

                // check if the item will break
                item::armor_status damaged = item::armor_status::UNDAMAGED;
                if( ablative_armor.find_armor_data()->non_functional != itype_id() ) {
                    // if the item transforms on destruction damage it that way
                    // ablative armor is concerned with incoming damage not mitigated damage
                    damaged = ablative_armor.damage_armor_transforms( pre_mitigation );
                } else {
                    damaged = ablative_armor.damage_armor_durability( du, bp->parent );
                }

                if( damaged == item::armor_status::TRANSFORMED ) {
                    //the plate is broken
                    const std::string pre_damage_name = ablative_armor.tname();

                    // TODO: add balistic and shattering verbs for ablative materials instead of hard coded
                    std::string format_string = _( "Your %1$s is %2$s!" );
                    std::string damage_verb = "shattered";
                    add_msg_if_player( m_bad, format_string, pre_damage_name, damage_verb );

                    if( is_avatar() ) {
                        SCT.add( point( posx(), posy() ), direction::NORTH, remove_color_tags( pre_damage_name ), m_neutral,
                                 damage_verb,
                                 m_info );
                    }

                    // delete the now destroyed item
                    itype_id replacement = ablative_armor.find_armor_data()->non_functional;
                    remove_item( ablative_armor );

                    // replace it with its destroyed version
                    pocket->add( item( replacement ) );

                    return true;
                }

                if( damaged == item::armor_status::DAMAGED ) {
                    //the plate is damaged like normal armor
                    describe_damage( du, ablative_armor );

                    return false;
                }

                if( damaged == item::armor_status::DESTROYED ) {
                    //the plate is damaged like normal armor but also ends up destroyed
                    describe_damage( du, ablative_armor );
                    if( get_player_view().sees( *this ) ) {
                        SCT.add( point( posx(), posy() ), direction::NORTH, remove_color_tags( ablative_armor.tname() ),
                                 m_neutral, _( "destroyed" ), m_info );
                    }
                    destroyed_armor_msg( *this, ablative_armor.tname() );

                    remove_item( ablative_armor );

                    return true;
                }

                return false;
            } else {
                // reduce value and try for additional plates
                roll = roll - coverage;
            }
        }
    }
    return false;

}

void Character::describe_damage( damage_unit &du, item &armor ) const
{
    const material_type &material = armor.get_random_material();
    std::string damage_verb = ( du.type == damage_type::BASH ) ? material.bash_dmg_verb() :
                              material.cut_dmg_verb();

    const std::string pre_damage_name = armor.tname();
    const std::string pre_damage_adj = armor.get_base_material().dmg_adj( armor.damage_level() );

    // add "further" if the damage adjective and verb are the same
    std::string format_string = ( pre_damage_adj == damage_verb ) ?
                                //~ %1$s is your armor name, %2$s is a damage verb
                                _( "Your %1$s is %2$s further!" ) : _( "Your %1$s is %2$s!" );
    add_msg_if_player( m_bad, format_string, pre_damage_name, damage_verb );
    //item is damaged
    if( is_avatar() ) {
        SCT.add( point( posx(), posy() ), direction::NORTH, remove_color_tags( pre_damage_name ), m_neutral,
                 damage_verb,
                 m_info );
    }
}

