#   Copyright (c) 2018 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 sys
import os
import re
import time
from random import random
from functools import reduce, partial

import numpy as np
import multiprocessing
import logging
import six
import re

import paddle
import paddle.fluid as F
import paddle.fluid.layers as L


from model.ernie import ErnieModel
from optimization import optimization
import utils.data

from propeller import log
log.setLevel(logging.DEBUG)
import propeller.paddle as propeller

class SequenceLabelErnieModel(propeller.train.Model):
    """propeller Model wraper for paddle-ERNIE """
    def __init__(self, hparam, mode, run_config):
        self.hparam = hparam
        self.mode = mode
        self.run_config = run_config
        self.num_label = len(hparam['label_list'])

    def forward(self, features):
        src_ids, sent_ids, input_seqlen = features
        zero = L.fill_constant([1], dtype='int64', value=0)
        input_mask = L.cast(L.equal(src_ids, zero), 'float32') # assume pad id == 0
        #input_mask = L.unsqueeze(input_mask, axes=[2])
        d_shape = L.shape(src_ids)
        seqlen = d_shape[1]
        batch_size = d_shape[0]
        pos_ids = L.unsqueeze(L.range(0, seqlen, 1, dtype='int32'), axes=[0])
        pos_ids = L.expand(pos_ids, [batch_size, 1])
        pos_ids = L.unsqueeze(pos_ids, axes=[2])
        pos_ids = L.cast(pos_ids, 'int64')
        pos_ids.stop_gradient = True
        input_mask.stop_gradient = True
        task_ids = L.zeros_like(src_ids) + self.hparam.task_id #this shit wont use at the moment
        task_ids.stop_gradient = True

        model = ErnieModel(
            src_ids=src_ids,
            position_ids=pos_ids,
            sentence_ids=sent_ids,
            task_ids=task_ids,
            input_mask=input_mask,
            config=self.hparam,
            use_fp16=self.hparam['use_fp16']
        )

        enc_out = model.get_sequence_output()
        logits = L.fc(
            input=enc_out,
            size=self.num_label,
            num_flatten_dims=2,
            param_attr= F.ParamAttr(
                name="cls_seq_label_out_w",
                initializer= F.initializer.TruncatedNormal(scale=0.02)),
            bias_attr=F.ParamAttr(
                name="cls_seq_label_out_b",
                initializer=F.initializer.Constant(0.)))

        propeller.summary.histogram('pred', logits)

        return logits, input_seqlen

    def loss(self, predictions, labels):
        logits, input_seqlen = predictions
        logits = L.flatten(logits, axis=2)
        labels = L.flatten(labels, axis=2)
        ce_loss, probs = L.softmax_with_cross_entropy(
            logits=logits, label=labels, return_softmax=True)
        loss = L.mean(x=ce_loss)
        return loss

    def backward(self, loss):
        scheduled_lr, _ = optimization(
            loss=loss,
            warmup_steps=int(self.run_config.max_steps * self.hparam['warmup_proportion']),
            num_train_steps=self.run_config.max_steps,
            learning_rate=self.hparam['learning_rate'],
            train_program=F.default_main_program(), 
            startup_prog=F.default_startup_program(),
            weight_decay=self.hparam['weight_decay'],
            scheduler="linear_warmup_decay",)
        propeller.summary.scalar('lr', scheduled_lr)

    def metrics(self, predictions, label):
        pred, seqlen = predictions
        pred = L.argmax(pred, axis=-1)
        pred = L.unsqueeze(pred, axes=[-1])
        f1 = propeller.metrics.ChunkF1(label, pred, seqlen, self.num_label)
        return {'f1': f1}

def make_sequence_label_dataset(name, input_files, label_list, tokenizer, batch_size, max_seqlen, is_train):
    label_map = {v: i for i, v in enumerate(label_list)}
    no_entity_id = label_map['O']
    delimiter = b''

    def read_bio_data(filename):
        ds = propeller.data.Dataset.from_file(filename)
        iterable = iter(ds)
        def gen():
            buf, size = [], 0
            iterator = iter(ds)
            while 1:
                line = next(iterator)
                cols = line.rstrip(b'\n').split(b'\t')
                tokens = cols[0].split(delimiter)
                labels = cols[1].split(delimiter)
                if len(cols) != 2:
                    continue
                if len(tokens) != len(labels) or len(tokens) == 0:
                    continue
                yield [tokens, labels]

        return propeller.data.Dataset.from_generator_func(gen)

    def reseg_token_label(dataset):
        def gen():
            iterator = iter(dataset)
            while True:
                tokens, labels = next(iterator)
                assert len(tokens) == len(labels)
                ret_tokens = []
                ret_labels = []
                for token, label in zip(tokens, labels):
                    sub_token = tokenizer(token)
                    label = label.decode('utf8')
                    if len(sub_token) == 0:
                        continue
                    ret_tokens.extend(sub_token)
                    ret_labels.append(label)
                    if len(sub_token) < 2:
                        continue
                    sub_label = label
                    if label.startswith("B-"):
                        sub_label = "I-" + label[2:]
                    ret_labels.extend([sub_label] * (len(sub_token) - 1))

                assert len(ret_tokens) == len(ret_labels)
                yield ret_tokens, ret_labels

        ds = propeller.data.Dataset.from_generator_func(gen)
        return ds

    def convert_to_ids(dataset):
        def gen():
            iterator = iter(dataset)
            while True:
                tokens, labels = next(iterator)
                if len(tokens) > max_seqlen - 2:
                    tokens = tokens[: max_seqlen - 2]
                    labels = labels[: max_seqlen - 2]

                tokens = ['[CLS]'] + tokens + ['[SEP]']
                token_ids = [vocab[t] for t in tokens]
                label_ids = [no_entity_id] + [label_map[x] for x in labels] + [no_entity_id]
                token_type_ids = [0] * len(token_ids)
                input_seqlen = len(token_ids)

                token_ids = np.array(token_ids, dtype=np.int64)
                label_ids = np.array(label_ids, dtype=np.int64)
                token_type_ids = np.array(token_type_ids, dtype=np.int64)
                input_seqlen = np.array(input_seqlen, dtype=np.int64)

                yield token_ids, token_type_ids, input_seqlen, label_ids

        ds = propeller.data.Dataset.from_generator_func(gen)
        return ds

    def after(*features):
        return utils.data.expand_dims(*features)

    dataset = propeller.data.Dataset.from_list(input_files)
    if is_train:
        dataset = dataset.repeat().shuffle(buffer_size=len(input_files))
    dataset = dataset.interleave(map_fn=read_bio_data, cycle_length=len(input_files), block_length=1)
    if is_train:
        dataset = dataset.shuffle(buffer_size=100)
    dataset = reseg_token_label(dataset)
    dataset = convert_to_ids(dataset)
    dataset = dataset.padded_batch(batch_size).map(after)
    dataset.name = name
    return dataset


def make_sequence_label_dataset_from_stdin(name, tokenizer, batch_size, max_seqlen):
    delimiter = b''

    def stdin_gen():
        if six.PY3:
            source = sys.stdin.buffer 
        else:
            source = sys.stdin
        while True:
            line = source.readline()
            if len(line) == 0:
                break
            yield line,

    def read_bio_data(ds):
        iterable = iter(ds)
        def gen():
            buf, size = [], 0
            iterator = iter(ds)
            while 1:
                line, = next(iterator)
                cols = line.rstrip(b'\n').split(b'\t')
                tokens = cols[0].split(delimiter)
                if len(cols) != 1:
                    continue
                if len(tokens) == 0:
                    continue
                yield tokens, 
        return propeller.data.Dataset.from_generator_func(gen)

    def reseg_token_label(dataset):
        def gen():
            iterator = iter(dataset)
            while True:
                tokens, = next(iterator)
                ret_tokens = []
                for token in tokens:
                    sub_token = tokenizer(token)
                    if len(sub_token) == 0:
                        continue
                    ret_tokens.extend(sub_token)
                    if len(sub_token) < 2:
                        continue
                yield ret_tokens, 
        ds = propeller.data.Dataset.from_generator_func(gen)
        return ds

    def convert_to_ids(dataset):
        def gen():
            iterator = iter(dataset)
            while True:
                tokens, = next(iterator)
                if len(tokens) > max_seqlen - 2:
                    tokens = tokens[: max_seqlen - 2]

                tokens = ['[CLS]'] + tokens + ['[SEP]']
                token_ids = [vocab[t] for t in tokens]
                token_type_ids = [0] * len(token_ids)
                input_seqlen = len(token_ids)

                token_ids = np.array(token_ids, dtype=np.int64)
                token_type_ids = np.array(token_type_ids, dtype=np.int64)
                input_seqlen = np.array(input_seqlen, dtype=np.int64)
                yield token_ids, token_type_ids, input_seqlen

        ds = propeller.data.Dataset.from_generator_func(gen)
        return ds

    def after(*features):
        return utils.data.expand_dims(*features)

    dataset = propeller.data.Dataset.from_generator_func(stdin_gen)
    dataset = read_bio_data(dataset)
    dataset = reseg_token_label(dataset)
    dataset = convert_to_ids(dataset)
    dataset = dataset.padded_batch(batch_size).map(after)
    dataset.name = name
    return dataset


if __name__ == '__main__':
    parser = propeller.ArgumentParser('NER model with ERNIE')
    parser.add_argument('--max_seqlen', type=int, default=128)
    parser.add_argument('--data_dir', type=str, required=True)
    parser.add_argument('--vocab_file', type=str, required=True)
    parser.add_argument('--do_predict', action='store_true')
    parser.add_argument('--use_sentence_piece_vocab', action='store_true')
    parser.add_argument('--warm_start_from', type=str)
    args = parser.parse_args()
    run_config = propeller.parse_runconfig(args)
    hparams = propeller.parse_hparam(args)


    vocab = {j.strip().split('\t')[0]: i for i, j in enumerate(open(args.vocab_file, 'r', encoding='utf8'))}
    tokenizer = utils.data.CharTokenizer(vocab, sentencepiece_style_vocab=args.use_sentence_piece_vocab)
    sep_id = vocab['[SEP]']
    cls_id = vocab['[CLS]']
    unk_id = vocab['[UNK]']
    pad_id = vocab['[PAD]']

    label_list = ['B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'O']
    hparams['label_list'] = label_list

    if not args.do_predict:
        train_data_dir = os.path.join(args.data_dir, 'train')
        train_input_files = [os.path.join(train_data_dir, filename) for filename in os.listdir(train_data_dir)]
        dev_data_dir = os.path.join(args.data_dir, 'dev')
        dev_input_files = [os.path.join(dev_data_dir, filename) for filename in os.listdir(dev_data_dir)]
        test_data_dir = os.path.join(args.data_dir, 'test')
        test_input_files = [os.path.join(test_data_dir, filename) for filename in os.listdir(test_data_dir)]

        train_ds = make_sequence_label_dataset(name='train', 
                                               input_files=train_input_files, 
                                               label_list=label_list, 
                                               tokenizer=tokenizer, 
                                               batch_size=hparams.batch_size, 
                                               max_seqlen=args.max_seqlen,
                                               is_train=True)
        dev_ds = make_sequence_label_dataset(name='dev',
                                             input_files=dev_input_files,
                                             label_list=label_list,
                                             tokenizer=tokenizer,
                                             batch_size=hparams.batch_size,
                                             max_seqlen=args.max_seqlen,
                                             is_train=False)
        test_ds = make_sequence_label_dataset(name='test',
                                              input_files=test_input_files,
                                              label_list=label_list,
                                              tokenizer=tokenizer,
                                              batch_size=hparams.batch_size,
                                              max_seqlen=args.max_seqlen,
                                              is_train=False)

        shapes = ([-1, args.max_seqlen, 1], [-1, args.max_seqlen, 1], [-1, 1], [-1, args.max_seqlen, 1]) 
        types = ('int64', 'int64', 'int64', 'int64')

        train_ds.data_shapes = shapes
        train_ds.data_types = types
        dev_ds.data_shapes = shapes
        dev_ds.data_types = types
        test_ds.data_shapes = shapes
        test_ds.data_types = types

        varname_to_warmstart = re.compile(r'^encoder.*[wb]_0$|^.*embedding$|^.*bias$|^.*scale$|^pooled_fc.[wb]_0$')
        warm_start_dir = args.warm_start_from
        ws = propeller.WarmStartSetting(
                predicate_fn=lambda v: varname_to_warmstart.match(v.name) and os.path.exists(os.path.join(warm_start_dir, v.name)),
                from_dir=warm_start_dir
            )

        best_exporter = propeller.train.exporter.BestInferenceModelExporter(os.path.join(run_config.model_dir, 'best'), cmp_fn=lambda old, new: new['dev']['f1'] > old['dev']['f1'])
        propeller.train.train_and_eval(
                model_class_or_model_fn=SequenceLabelErnieModel,
                params=hparams, 
                run_config=run_config, 
                train_dataset=train_ds, 
                eval_dataset={'dev': dev_ds, 'test': test_ds}, 
                warm_start_setting=ws, 
                exporters=[best_exporter])

        for k in best_exporter._best['dev'].keys():
            if 'loss' in k:
                continue
            dev_v = best_exporter._best['dev'][k]
            test_v = best_exporter._best['test'][k]
            print('dev_%s\t%.5f\ntest_%s\t%.5f' % (k, dev_v, k, test_v))
    else:
        predict_ds = make_sequence_label_dataset_from_stdin(name='pred', 
                                               tokenizer=tokenizer, 
                                               batch_size=hparams.batch_size, 
                                               max_seqlen=args.max_seqlen)

        shapes = ([-1, args.max_seqlen, 1], [-1, args.max_seqlen, 1], [-1, 1]) 
        types = ('int64', 'int64', 'int64')

        predict_ds.data_shapes = shapes
        predict_ds.data_types = types

        rev_label_map = {i: v for i, v in enumerate(label_list)}
        learner = propeller.Learner(SequenceLabelErnieModel, run_config, hparams)
        for pred, _  in learner.predict(predict_ds, ckpt=-1):
            pred_str = ' '.join([rev_label_map[idx] for idx in np.argmax(pred, 1).tolist()])
            print(pred_str)
            

