import scheduling
import logging

from ppms import project_rights, ppms_cu, ppms
from ppms import resource_approval
from ppms.calculations.budget.budget_calc_portfolio import start_budget_calc_pf
from ppms.calculations.budget.budget_calc_program import calculate_all_programs
from ppms.calculations.budget.budget_calc_project import BudgetCalcProject
from ppms.calculations.budget.budget_helper import get_plan_objects_in_mod, get_pr_tree_desc
from ppms.calculations.risks_and_chances.calc_project_totals import RiskChanceCalculation
from ppms.constants import *
from ppms.ctg_aggregation import CostCalculation
from ppms.text_constant import *
from ppms.cu_access import get_dt_of_ddi


logger = logging.getLogger(__name__)


def start_tcalc(mod_obj, pr_list=[], calc_portfolios=False):
    """ Start tcalc and cost+budget calculations.
    mod_obj: The module to operate on.
                - In case pr_list is not set, the objects to calculate are searched in this mod
                - It's used to determine whether a single planning, or a complete new-planning will be executed

    pr_list (optional): List of project ids to calculate
    calc_portfolios (optional): Special parameter used for new-planning. If set to True it will calculate
                                costs+budget of all portfolios additionally to the other objects.

    returns True/False whether calculations were done. This is used to eg. stop report creation when scheduling
    wasn't possible.
    """
    mod_id = mod_obj.get_id()
    new_planning = is_new_planning_mod(mod_id)

    # get the module to calculate on
    mod_obj = get_target_module(mod_obj)
    if not mod_obj:
        return False

    if project_rights.check_for_active_planing():
        # stop if "Neuplanung" or any single scheduling is running
        ppms.ui_message_id("0200")
        return False

    mod_obj.menu(34)

    if not pr_list:
        pr_list = get_plan_objects_in_mod(mod_obj)

    if new_planning:
        start_new_planning(mod_obj, pr_list, calc_portfolios)
    else:
        start_single_calculation(mod_obj, pr_list)

        refresh_module_tree(mod_obj)
        show_changes_in_panel(mod_obj)

    return True


def start_new_planning(mod_obj, pr_list, calc_portfolios):
    """ Execute "Neuplanung" and recalc costs+budget for all projects+programs. """

    # start "Neuplanung"
    scheduling.reschedule(pr_list)

    # Start cost calculation of all projects
    CostCalculation(mod_obj=mod_obj).start_full_aggregation()

    # Start budget calculation of all projects
    BudgetCalcProject(pr_list).start_budget_calc()

    RiskChanceCalculation().start()

    # Start scheduling and cost+budget calculation of all programs
    calculate_all_programs()

    # Optional: calc cost+budget for all portfolios
    if calc_portfolios:
        start_budget_calc_pf()

    if mod_obj:
        mod_obj.set_statusbar(ppms_cu.Access().get_text_constant('001828'))


def start_scheduling(pr_list):
    if project_rights.check_cost_details_toggle():
        # only time scheduling (manual cost registration)
        scheduling.schedule_time(pr_list)
    else:
        # time and capacity scheduling (automatic cost calculation from DT472)
        scheduling.schedule_capacity(pr_list)


def start_single_calculation(mod_obj, pr_list):
    """ Execute tcalc for one (or more) objects in pr_list and recalc costs+budget for them. """
    start_scheduling(pr_list)

    # cost aggregation for all projects in the pr_list
    cost_aggregation = init_cost_aggregation(mod_obj, pr_list)
    if cost_aggregation:
        cost_aggregation.start_full_aggregation()
        risk_calc = RiskChanceCalculation()
        risk_calc.start(pr_list)

    # calc psp code
    if str(ppms_cu.Helper.get_global_setting("show_psp_instead_of_id").alpha120.get_value()) == "1":
        if hasattr(mod_obj, 'generate_psp_code'):
            mod_obj.generate_psp_code()

    # calc program costs
    program_cost_calculation(mod_obj)

    # calc resource request differences
    if resource_approval.calc_effort_to_approval_differences(pr_list):
        mod_obj.menu(MENU_RESET)

    mod_obj.set_statusbar(get_text_constant('001829'))


def program_cost_calculation(mod_obj):
    """ Calculate program costs and budget if tcalc is started in the program module."""
    panel_obj = mod_obj.get_panel()
    program_startmodule = ppms_cu.Helper.get_global_setting("programm_startmodule").alpha120.get_value()
    main_mod_id = get_main_mod_id(panel_obj)
    if main_mod_id != program_startmodule:
        return

    l_var = mod_obj.get_current_L_var()
    program_id = l_var.get(30, None)
    if not program_id:
        return

    # import here to prevent circular import
    from ppms.calculations.budget.budget_calc_program import BudgetCalcProgram
    # calculate program costs and budget
    BudgetCalcProgram(program_id[0]).start_budget_calc_program()


def show_changes_in_panel(mod_obj):
    """ Show the "refresh" button in all modules in panel which need it. """
    panel = mod_obj.get_panel()
    if not panel:
        return
    for panel_mod_obj in panel.get_modules():
        if panel_mod_obj.is_stub():
            # don't need to refresh if the mod hasn't been loaded yet
            continue
        if hasattr(panel_mod_obj, 'changes_in_panel'):
            panel_mod_obj.changes_in_panel = True


def refresh_module_tree(mod_obj):
    """ refresh dt472 records (loads) in the module."""
    if load_table_is_visible(mod_obj):
        logger.info(f'Load is visible in module {mod_obj}, refreshing')
        mod_obj.refresh_dts((204, 461, 472))

    # invoke menu 19 on child modules
    if hasattr(mod_obj, 'actualize_child_modules'):
        mod_obj.actualize_child_modules()


def get_main_mod_id(panel_obj):
    """ Tries to find the id of the main module in the panel.
    If tcalc is used outside the project workflow, the invoker could be
    the user menu. Therefore assuming the main mod is the user menu,
    in case panel can't be found."""
    if panel_obj:
        main_mod_id = panel_obj.get_main_module().get_id()
    else:
        main_mod_id = ppms_cu.Helper.get_global_setting("user_menu").alpha120.get_value()

    return main_mod_id


def is_new_planning_mod(mod_id):
    """ Returns boolean whether given mod_id is the new-planning module."""
    mod_rec = ppms.search_record(405, [mod_id], ['sub_class'])
    new_planning_sub_class = 2
    return mod_rec.sub_class.get_value() == new_planning_sub_class


def is_program(pr_id):
    """ Returns boolean whether given pr_id is a program."""
    pr_rec = ppms.search_record(461, [pr_id], ['pr_type'])
    if pr_rec.pr_type.get_value() == 30:
        return True
    return False


def init_cost_aggregation(mod_obj, pr_list):
    """ Initialize cost aggregation. """

    # remove programs from list
    pr_list = [pr_id for pr_id in pr_list if not is_program(pr_id)]

    # add sub projects to list if only one pr_id is given
    if len(pr_list) == 1:
        pr_list = get_pr_tree_desc(pr_list[0])

    if pr_list:
        cost_aggregation = CostCalculation(projects=pr_list,
                                           portfolios=None,
                                           mod_obj=mod_obj)
        return cost_aggregation


def get_target_module(mod_obj):
    """ As start_tcalc is usually called from a macro (0099TT, or a custom one),
    we need to get the the underlying data module here."""
    mod_obj_type = ppms_cu.get_mod_obj_type(mod_obj)
    if mod_obj_type == MODULE_CLASS_MACRO:
        mod_obj = mod_obj.get_invoker_module()
        title = "Wrong function call"
        implement_info = 'Please check your implementation of start_tcalc().' \
                         ' The invoker module of your macro has to be the data module ' \
                         'containing your project(s).'

        if not mod_obj:
            msg_text = 'Invoker module of the scheduling macro could not be ' \
                       'found.\n\n{implement_info}'.format(**locals())
            ppms.ui_message_box(title, msg_text, ole_id=OLE_STOP)
            return

        mod_obj_type = ppms_cu.get_mod_obj_type(mod_obj)
        if mod_obj_type != MODULE_CLASS_DATA_HANDLING:
            msg_text = 'The module {mod_obj} is not a data module.\n\n' \
                       '{implement_info}'.format(**locals())
            ppms.ui_message_box(title, msg_text, ole_id=OLE_STOP)
            return
    return mod_obj


def load_table_is_visible(mod_obj):
    """Returns true if the module uses the load table DT472 in its current state (module variant), or false if not."""
    load_tables = [472, 204]

    for da_obj in mod_obj.get_das():
        da_cu = da_obj.get_customizing()
        if da_cu.is_never_display():
            continue

        da_dt = get_dt_of_ddi(da_cu.get_id())
        if da_dt in load_tables:
            return True

    return False
