from generator import Generator
from discriminator import Discriminator
from speaker_encoder import SPEncoder
import torch
import torch.nn.functional as F
import os
from os.path import join, basename, exists
import time
import datetime
import numpy as np
from tqdm import tqdm
import numpy as np
import copy

class Solver(object):

    def __init__(self, train_loader, config):
        """Initialize configurations."""

        self.train_loader = train_loader
        self.sampling_rate = config.sampling_rate

        self.D_name = config.discriminator
        self.SPE_name = config.spenc
        self.G_name = config.generator
        
        self.g_hidden_size = config.g_hidden_size
        self.num_speakers = config.num_speakers
        self.spk_emb_dim = config.spk_emb_dim
            
        self.lambda_rec = config.lambda_rec
        self.lambda_id = config.lambda_id
        self.lambda_adv = config.lambda_adv    
        
        
        self.batch_size = config.batch_size
        self.num_iters = config.num_iters
        self.g_lr = config.g_lr
        self.d_lr = config.d_lr
        self.beta1 = config.beta1
        self.beta2 = config.beta2
        self.resume_iters = config.resume_iters
        self.use_ema = config.use_ema
        self.auto_resume = config.auto_resume
        
        self.kernel = config.kernel
        self.num_heads = config.num_heads
        self.num_res_blocks = config.num_res_blocks
        
        
        self.use_tensorboard = config.use_tensorboard
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

        self.log_dir = config.log_dir
        self.model_save_dir = config.model_save_dir

        self.log_step = config.log_step
        self.sample_step = config.sample_step
        self.model_save_step = config.model_save_step

        self.build_model()
        if self.use_tensorboard:
            self.build_tensorboard()

    def build_model(self):
        """Create a generator and a discriminator."""
        self.generator = eval(self.G_name)(num_speakers=self.num_speakers, 
                                                kernel = self.kernel, 
                                                num_heads = self.num_heads, 
                                                num_res_blocks = self.num_res_blocks, 
                                                spk_emb_dim = self.spk_emb_dim,
                                                )
        self.discriminator = eval(self.D_name)(num_speakers=self.num_speakers)
        self.sp_enc = eval(self.SPE_name)(num_speakers = self.num_speakers, spk_emb_dim = self.spk_emb_dim)
        self.sp_enc.to(self.device)
        self.generator.to(self.device)
        self.discriminator.to(self.device)
        
        g_params = list(self.generator.parameters())
        g_params += list(self.sp_enc.parameters())
        d_params = list(self.discriminator.parameters())
        
        self.g_optimizer = torch.optim.Adam(g_params, self.g_lr, [self.beta1, self.beta2])
        self.d_optimizer = torch.optim.Adam(d_params, self.d_lr, [self.beta1, self.beta2])
        # restore model
        if not self.auto_resume:
            if  self.resume_iters and not self.resume_ft: 
                print("resuming step %d ..."% self.resume_iters, flush=True)
                self.restore_model(self.resume_iters)
        else:
            ckpt_files = [ int(x.split('-')[0]) for x in os.listdir(self.model_save_dir)]
            last_step = sorted(ckpt_files, reverse = True)[0]
            print("auto resuming step %d ..."% last_step, flush=True)
            self.restore_model(last_step)
            self.resume_iters = last_step

        if self.use_ema:
            self.generator_ema = copy.deepcopy(self.generator)
            self.sp_enc_ema = copy.deepcopy(self.sp_enc)

        self.print_network(self.generator, 'Generator')
        self.print_network(self.discriminator, 'Discriminator')
        self.print_network(self.sp_enc, 'SpeakerEncoder')

        if self.use_ema:
            self.generator_ema.to(self.device)
            self.sp_enc_ema.to(self.device)
    def print_network(self, model, name):
        """Print out the network information."""
        num_params = 0
        for p in model.parameters():
            num_params += p.numel()
        print(model, flush=True)
        print(name,flush=True)
        print("The number of parameters: {}".format(num_params), flush=True)
    
    def moving_average(self, model, model_test, beta = 0.999):
        for param, param_test in zip(model.parameters(), model_test.parameters()):
            param_test.data  = torch.lerp(param.data, param_test.data, beta)

    def restore_model(self, resume_iters, resume_ft = False):
        """Restore the trained generator and discriminator."""
        print('Loading the trained models from step {}...'.format(resume_iters), flush=True)
        g_path = os.path.join(self.model_save_dir, '{}-G.ckpt'.format(resume_iters))
        d_path = os.path.join(self.model_save_dir, '{}-D.ckpt'.format(resume_iters))
        sp_path = os.path.join(self.model_save_dir, '{}-sp.ckpt'.format(resume_iters))
        
        g_opt_path = os.path.join(self.model_save_dir, '{}-g_opt.ckpt'.format(resume_iters))
        d_opt_path = os.path.join(self.model_save_dir, '{}-d_opt.ckpt'.format(resume_iters))
        

        self.generator.load_state_dict(torch.load(g_path, map_location=lambda storage, loc: storage))
        self.discriminator.load_state_dict(torch.load(d_path, map_location=lambda storage, loc: storage))
        self.sp_enc.load_state_dict(torch.load(sp_path, map_location=lambda storage, loc: storage))
        print("loading optimizer",flush=True)
        if exists(g_opt_path):
            self.g_optimizer.load_state_dict(torch.load(g_opt_path, map_location = lambda storage, loc: storage))
        if exists(d_opt_path):
            self.d_optimizer.load_state_dict(torch.load(d_opt_path, map_location = lambda storage, loc: storage))

    def build_tensorboard(self):
        """Build a tensorboard logger."""
        from logger import Logger
        self.logger = Logger(self.log_dir)

    def update_lr(self, g_lr, d_lr):
        """Decay learning rates of the generator and discriminator."""
        for param_group in self.g_optimizer.param_groups:
            param_group['lr'] = g_lr
        for param_group in self.d_optimizer.param_groups:
            param_group['lr'] = d_lr

    def reset_grad(self):
        """Reset the gradientgradient buffers."""
        self.g_optimizer.zero_grad()
        self.d_optimizer.zero_grad()


    def label2onehot(self, labels, dim):
        """Convert label indices to one-hot vectors."""
        batch_size = labels.size(0)
        out = torch.zeros(batch_size, dim)
        out[np.arange(batch_size), labels.long()] = 1
        return out

    def sample_spk_c(self, size):
        spk_c = np.random.randint(0, self.num_speakers, size=size)
        spk_c_cat = to_categorical(spk_c, self.num_speakers)
        return torch.LongTensor(spk_c), torch.FloatTensor(spk_c_cat)

    def classification_loss(self, logit, target):
        """Compute softmax cross entropy loss."""
        return F.cross_entropy(logit, target)

    def load_wav(self, wavfile, sr=16000):
        wav, _ = librosa.load(wavfile, sr=sr, mono=True)
        return wav_padding(wav, sr=16000, frame_period=5, multiple = 4)

    def load_mel(self, melfile):
        tmp_mel = np.load(melfile)
        return tmp_mel
    def train(self):
        # Set data loader.
        train_loader = self.train_loader
        data_iter = iter(train_loader)


        g_lr = self.g_lr
        d_lr = self.d_lr

        start_iters = 0
        if self.resume_iters:
            start_iters = self.resume_iters

        print('Start training...', flush=True)
        start_time = time.time()
        for i in range(start_iters, self.num_iters):
            try:
                mc_src, spk_label_org, spk_c_org, mc_trg, spk_label_trg, spk_c_trg = next(data_iter)
            except:
                data_iter = iter(train_loader)
                mc_src, spk_label_org, spk_c_org, mc_trg, spk_label_trg, spk_c_trg = next(data_iter)
        
            mc_src.unsqueeze_(1) 
            mc_trg.unsqueeze_(1) 


            mc_src = mc_src.to(self.device)              
            mc_trg = mc_trg.to(self.device)              
            spk_label_org = spk_label_org.to(self.device)  
            spk_c_org = spk_c_org.to(self.device)          
            spk_label_trg = spk_label_trg.to(self.device)  
            spk_c_trg = spk_c_trg.to(self.device)          

                
                
            spk_c_trg = self.sp_enc(mc_trg, spk_label_trg)
        
            spk_c_org = self.sp_enc(mc_src, spk_label_org)
            
            d_out_src = self.discriminator(mc_src, spk_label_trg, spk_label_org)
            d_loss_real = torch.mean(  (1.0 - d_out_src)**2  )


            mc_fake = self.generator(mc_src, spk_c_org, spk_c_trg)
            d_out_fake = self.discriminator(mc_fake.detach(), spk_label_org, spk_label_trg)
            d_loss_fake = torch.mean(d_out_fake ** 2)
            
            # Backward and optimize.
            d_loss = d_loss_real + d_loss_fake
            self.reset_grad()
            d_loss.backward()
            self.d_optimizer.step()

            # Logging.
            loss = {}
            loss['D/loss_real'] = d_loss_real.item()
            loss['D/loss_fake'] = d_loss_fake.item()
            loss['D/loss'] = d_loss.item()
            
                
            spk_c_trg = self.sp_enc(mc_trg, spk_label_trg)
            spk_c_org = self.sp_enc(mc_src, spk_label_org)

            
            mc_fake = self.generator(mc_src, spk_c_org,  spk_c_trg)
            g_out_src = self.discriminator(mc_fake, spk_label_org, spk_label_trg)
            g_loss_fake = torch.mean((1.0 - g_out_src)**2)
            
            mc_reconst = self.generator(mc_fake, spk_c_trg, spk_c_org)
            g_loss_rec = torch.mean(torch.abs(mc_src - mc_reconst))

            mc_fake_id = self.generator(mc_src, spk_c_org, spk_c_org)
            g_loss_id = torch.mean(torch.abs(mc_src - mc_fake_id))
            

            # Backward and optimize.
            g_loss = self.lambda_adv *  g_loss_fake \
                + self.lambda_rec * g_loss_rec \
                + self.lambda_id * g_loss_id 
            

            self.reset_grad()
            g_loss.backward()
                
            self.g_optimizer.step()
            # Logging.
            loss['G/loss_fake'] = g_loss_fake.item()
            loss['G/loss_rec'] = g_loss_rec.item()
            loss['G/loss_id'] = g_loss_id.item()
            if self.use_ema:
                self.moving_average(self.generator, self.generator_ema)
                self.moving_average(self.sp_enc, self.sp_enc_ema)

            if (i+1) % self.log_step == 0:
                et = time.time() - start_time
                et = str(datetime.timedelta(seconds=et))[:-7]
                log = "Elapsed [{}], Iteration [{}/{}]".format(et, i+1, self.num_iters)
                for tag, value in loss.items():
                    log += ", {}: {:.4f}".format(tag, value)
                print(log, flush=True)

                if self.use_tensorboard:
                    for tag, value in loss.items():
                        self.logger.scalar_summary(tag, value, i+1)
                
            if (i+1) % self.model_save_step == 0:
                g_path = os.path.join(self.model_save_dir, '{}-G.ckpt'.format(i+1))
                g_path_ema = os.path.join(self.model_save_dir, '{}-G.ckpt.ema'.format(i+1))
                d_path = os.path.join(self.model_save_dir, '{}-D.ckpt'.format(i+1))
                sp_path = os.path.join(self.model_save_dir, '{}-sp.ckpt'.format(i+1))
                sp_path_ema = os.path.join(self.model_save_dir, '{}-sp.ckpt.ema'.format(i+1))
                

                g_opt_path = os.path.join(self.model_save_dir, '{}-g_opt.ckpt'.format(i+1))
                d_opt_path = os.path.join(self.model_save_dir, '{}-d_opt.ckpt'.format(i+1))

                torch.save(self.generator.state_dict(), g_path)
                if self.use_ema:
                    torch.save(self.generator_ema.state_dict(), g_path_ema)
                torch.save(self.discriminator.state_dict(), d_path)
                torch.save(self.sp_enc.state_dict(), sp_path)
                if self.use_ema:
                    torch.save(self.sp_enc_ema.state_dict(), sp_path_ema)
                torch.save(self.g_optimizer.state_dict(), g_opt_path)
                torch.save(self.d_optimizer.state_dict(), d_opt_path)
                print('Saved model checkpoints into {}...'.format(self.model_save_dir), flush=True)
            
            

