import torch.nn as nn
import torch.optim as optim
import torch.optim.lr_scheduler as lr_sched
from functools import partial
from .fastai_optim import OptimWrapper
from .learning_schedules_fastai import OneCycle, CosineWarmupLR


def build_optimizer(model, optim_cfg):
    if optim_cfg.OPTIMIZER == 'adam':
        optimizer = optim.Adam(model.parameters(), lr=optim_cfg.LR, weight_decay=optim_cfg.WEIGHT_DECAY)
    elif optim_cfg.OPTIMIZER == 'sgd':
        optimizer = optim.SGD(
            model.parameters(), lr=optim_cfg.LR, weight_decay=optim_cfg.WEIGHT_DECAY,
            momentum=optim_cfg.MOMENTUM
        )
    elif optim_cfg.OPTIMIZER == 'adam_onecycle':
        def children(m: nn.Module):
            return list(m.children())

        def num_children(m: nn.Module) -> int:
            return len(children(m))

        flatten_model = lambda m: sum(map(flatten_model, m.children()), []) if num_children(m) else [m]
        get_layer_groups = lambda m: [nn.Sequential(*flatten_model(m))]

        optimizer_func = partial(optim.Adam, betas=(0.9, 0.99))
        optimizer = OptimWrapper.create(
            optimizer_func, 3e-3, get_layer_groups(model), wd=optim_cfg.WEIGHT_DECAY, true_wd=True, bn_wd=True
        )
    else:
        raise NotImplementedError

    return optimizer


def build_scheduler(optimizer, total_iters_each_epoch, total_epochs, last_epoch, optim_cfg):
    """Build a lr scheduler and a lr warm up scheduler for the optimizer to control the LR.

    If optim_cfg.OPTIMIZER is adam_onecycle, only the adam_onecyle lr scheduler is returned,
    without any warm up lr scheduler.

    Otherwise, both lr scheduler and warm up lr scheduler are returned.

    Args:
        optimizer (): The PyTorch Optimizer that we are using for this experiment.
        total_iters_each_epoch (): The number of iteration per epoch.
        total_epochs (): The ending epoch of this experiment.
        last_epoch (): The starting epoch of this experiment. Useful if we are resuming.
        optim_cfg (): A dictionary of cfg for the optimizer.

    Returns:
        lr_scheduler (): A scheduler to control LR.
        lr_warmup_scheduler (): A scheudler to control LR for the initial warm up period.
            Note that during training, this scheduler will only be picked if
                - optim_cfg.LR_WARMUP is True
                - current epoch is less than optim_cfg.WARMUP_EPOCH (warm up period)

    """

    # the train steps at which to decay the lr by a multiplicative factor
    decay_steps = [x * total_iters_each_epoch for x in optim_cfg.DECAY_STEP_LIST]

    # the function that controls the lr following the decay_steps, used by lr_sched.LambdaLR
    def lr_lbmd(cur_epoch):
        cur_decay = 1
        for decay_step in decay_steps:
            if cur_epoch >= decay_step:
                cur_decay = cur_decay * optim_cfg.LR_DECAY
        return max(cur_decay, optim_cfg.LR_CLIP / optim_cfg.LR)

    lr_warmup_scheduler = None
    total_steps = total_iters_each_epoch * total_epochs

    if optim_cfg.OPTIMIZER == 'adam_onecycle':
        lr_scheduler = OneCycle(
            optimizer, total_steps, optim_cfg.LR, list(optim_cfg.MOMS), optim_cfg.DIV_FACTOR, optim_cfg.PCT_START
        )
    else:
        lr_scheduler = lr_sched.LambdaLR(optimizer, lr_lbmd, last_epoch=last_epoch)

        if optim_cfg.LR_WARMUP:
            lr_warmup_scheduler = CosineWarmupLR(
                optimizer, T_max=optim_cfg.WARMUP_EPOCH * total_iters_each_epoch,
                eta_min=optim_cfg.LR / optim_cfg.DIV_FACTOR
            )

    return lr_scheduler, lr_warmup_scheduler
