# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import random
import shutil
import tempfile
import time
import unittest

import paddle
import paddle.vision.transforms as T
from paddle.distributed.auto_parallel.callbacks import config_callbacks
from paddle.distributed.fleet import auto
from paddle.static import InputSpec
from paddle.vision.datasets import MNIST
from paddle.vision.models import LeNet

paddle.enable_static()


class TestCallbacks(unittest.TestCase):
    def setUp(self):
        self.save_dir = tempfile.mkdtemp()

    def tearDown(self):
        shutil.rmtree(self.save_dir)

    def run_callback(self):
        epochs = 2
        steps = 5
        freq = 2
        eval_steps = 2

        inputs_spec = [InputSpec([None, 1, 28, 28], 'float32', 'image')]
        strategy = auto.Strategy()
        strategy.auto_mode = "semi"

        engine = auto.Engine(LeNet(), strategy=strategy)
        engine.prepare(inputs_spec, mode="predict")

        cbks = config_callbacks(
            engine=engine,
            batch_size=128,
            epochs=epochs,
            steps=steps,
            log_freq=freq,
            verbose=self.verbose,
            metrics=['loss', 'acc'],
            save_dir=self.save_dir,
        )
        cbks.on_begin('train')

        logs = {'loss': 50.341673, 'acc': 0.00256}
        for epoch in range(epochs):
            cbks.on_epoch_begin(epoch)
            for step in range(steps):
                cbks.on_batch_begin('train', step, logs)
                logs['loss'] -= random.random() * 0.1
                logs['acc'] += random.random() * 0.1
                time.sleep(0.005)
                cbks.on_batch_end('train', step, logs)
            cbks.on_epoch_end(epoch, logs)

            eval_logs = {'eval_loss': 20.341673, 'eval_acc': 0.256}
            params = {
                'steps': eval_steps,
                'metrics': ['eval_loss', 'eval_acc'],
            }
            cbks.on_begin('eval', params)
            for step in range(eval_steps):
                cbks.on_batch_begin('eval', step, eval_logs)
                eval_logs['eval_loss'] -= random.random() * 0.1
                eval_logs['eval_acc'] += random.random() * 0.1
                eval_logs['batch_size'] = 2
                time.sleep(0.005)
                cbks.on_batch_end('eval', step, eval_logs)
            cbks.on_end('eval', eval_logs)

            test_logs = {}
            params = {'steps': eval_steps}
            cbks.on_begin('predict', params)
            for step in range(eval_steps):
                cbks.on_batch_begin('predict', step, test_logs)
                test_logs['batch_size'] = 2
                time.sleep(0.005)
                cbks.on_batch_end('predict', step, test_logs)
            cbks.on_end('predict', test_logs)

        cbks.on_end('train')

        print(engine.history.history)

    def test_callback_verbose_0(self):
        self.verbose = 0
        self.run_callback()

    def test_callback_verbose_1(self):
        self.verbose = 1
        self.run_callback()

    def test_callback_verbose_2(self):
        self.verbose = 2
        self.run_callback()

    def test_callback_verbose_3(self):
        self.verbose = 3
        self.run_callback()


class TestCallbacksEngine(unittest.TestCase):
    def setUp(self):
        self.save_dir = tempfile.mkdtemp()
        transform = T.Compose([T.Transpose(), T.Normalize([127.5], [127.5])])
        self.train_dataset = MNIST(mode='train', transform=transform)
        self.test_dataset = MNIST(mode='test', transform=transform)
        self.prepare_engine()

    def tearDown(self):
        shutil.rmtree(self.save_dir)

    def prepare_engine(self):
        model = paddle.vision.models.LeNet()
        loss = paddle.nn.CrossEntropyLoss()
        base_lr = 1e-3
        boundaries = [5, 8]
        values = [base_lr * (0.1**i) for i in range(len(boundaries) + 1)]
        lr = paddle.optimizer.lr.PiecewiseDecay(
            boundaries=boundaries, values=values, verbose=False
        )
        optimizer = paddle.optimizer.Adam(
            learning_rate=lr, parameters=model.parameters()
        )
        auto.fetch(model.parameters()[0], "param0", logging=True)
        metrics = paddle.metric.Accuracy(topk=(1, 2))
        self.engine = auto.Engine(model, loss, optimizer, metrics)

    def test_fit_eval(self):
        history = self.engine.fit(
            train_data=self.train_dataset,
            valid_data=self.test_dataset,
            batch_size=128,
            steps_per_epoch=60,
            valid_steps=40,
            log_freq=20,
            save_dir=self.save_dir,
            save_freq=1,
        )
        print(history.history)

    def test_eval(self):
        self.engine.evaluate(
            valid_data=self.test_dataset, batch_size=128, steps=40, log_freq=10
        )

    def test_predict(self):
        logger_cbks = paddle.callbacks.ProgBarLogger()
        self.engine.predict(
            test_data=self.test_dataset, batch_size=128, callbacks=[logger_cbks]
        )


if __name__ == '__main__':
    unittest.main()
