#!/user/bin/env python
# Copyright (c) 2017 VisualDL Authors. All Rights Reserve.
#
# 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 functools
import gzip
import json
import os
from io import BytesIO

from flask import request

from visualdl import LogReader
from visualdl.python.cache import MemCache
from visualdl.reader.graph_reader import GraphReader
from visualdl.server import lib
from visualdl.server.client_manager import ClientManager
from visualdl.server.log import logger

error_retry_times = 3
error_sleep_time = 2  # seconds


def gen_result(data=None, status=0, msg=''):
    return {'status': status, 'msg': msg, 'data': data}


def result(mimetype='application/json', headers=None):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            data = None
            status = 0
            msg = ''
            try:
                data = func(self, *args, **kwargs)
            except Exception as e:
                msg = '{}'.format(e)
                status = -1
            if mimetype == 'application/json':
                data = json.dumps(gen_result(data, status, msg))
            if callable(headers):
                headers_output = headers(self)
            else:
                headers_output = headers
                if headers is not None:
                    if 'content-encoding' in headers:
                        buf = BytesIO()
                        with gzip.GzipFile(mode='wb', fileobj=buf) as fp:
                            gzip_value = data.encode()
                            fp.write(gzip_value)
                        data = buf.getvalue()
            return data, mimetype, headers_output

        return wrapper

    return decorator


def try_call(function, *args, **kwargs):
    res = lib.retry(error_retry_times, function, error_sleep_time, *args,
                    **kwargs)
    if not res:
        logger.error("Internal server error. Retry later.")
    return res


class Api(object):
    def __init__(self, logdir, model, cache_timeout):
        self._reader = LogReader(logdir)
        self._graph_reader = GraphReader(logdir)
        self._graph_reader.set_displayname(self._reader)
        if model:
            if 'vdlgraph' in model:
                self._graph_reader.set_input_graph(model)
            self._reader.model = model
            self.model_name = os.path.basename(model)
        else:
            self.model_name = ''
        self.graph_reader_client_manager = ClientManager(self._graph_reader)
        # use a memory cache to reduce disk reading frequency.
        cache = MemCache(timeout=cache_timeout)
        self._cache = lib.cache_get(cache)

    def _get(self, key, func, *args, **kwargs):
        return self._cache(key, func, self._reader, *args, **kwargs)

    def _get_with_retry(self, key, func, *args, **kwargs):
        return self._cache(key, try_call, func, self._reader, *args, **kwargs)

    @result()
    def components(self):
        return self._get('data/components', lib.get_components)

    def component_tabs(self):
        '''
        Get all component tabs supported by readers in Api.
        '''
        tabs = set()
        tabs.update(self._reader.component_tabs(update=True))
        tabs.update(self._graph_reader.component_tabs(update=True))
        return tabs

    @result()
    def runs(self):
        return self._get('data/runs', lib.get_runs)

    @result()
    def graph_runs(self):
        client_ip = request.remote_addr
        graph_reader = self.graph_reader_client_manager.get_data(client_ip)
        return lib.get_graph_runs(graph_reader)

    @result()
    def tags(self):
        return self._get('data/tags', lib.get_tags)

    @result()
    def logs(self):
        return self._get('data/logs', lib.get_logs)

    @result()
    def scalar_tags(self):
        return self._get_with_retry('data/plugin/scalars/tags',
                                    lib.get_scalar_tags)

    @result()
    def scalars_tags(self):
        return self._get_with_retry('data/plugin/multiscalars/tags',
                                    lib.get_scalars_tags)

    @result()
    def image_tags(self):
        return self._get_with_retry('data/plugin/images/tags',
                                    lib.get_image_tags)

    @result()
    def text_tags(self):
        return self._get_with_retry('data/plugin/text/tags', lib.get_text_tags)

    @result()
    def audio_tags(self):
        return self._get_with_retry('data/plugin/audio/tags',
                                    lib.get_audio_tags)

    @result()
    def embedding_tags(self):
        return self._get_with_retry('data/plugin/embeddings/tags',
                                    lib.get_embeddings_tags)

    @result()
    def pr_curve_tags(self):
        return self._get_with_retry('data/plugin/pr_curves/tags',
                                    lib.get_pr_curve_tags)

    @result()
    def roc_curve_tags(self):
        return self._get_with_retry('data/plugin/roc_curves/tags',
                                    lib.get_roc_curve_tags)

    @result()
    def hparam_importance(self):
        return self._get_with_retry('data/plugin/hparams/importance',
                                    lib.get_hparam_importance)

    @result()
    def hparam_indicator(self):
        return self._get_with_retry('data/plugin/hparams/indicators',
                                    lib.get_hparam_indicator)

    @result()
    def hparam_list(self):
        return self._get_with_retry('data/plugin/hparams/list',
                                    lib.get_hparam_list)

    @result()
    def hparam_metric(self, run, metric):
        key = os.path.join('data/plugin/hparams/metric', run, metric)
        return self._get_with_retry(key, lib.get_hparam_metric, run, metric)

    @result('text/csv')
    def hparam_data(self, type='tsv'):
        key = os.path.join('data/plugin/hparams/data', type)
        return self._get_with_retry(key, lib.get_hparam_data, type)

    @result()
    def scalar_list(self, run, tag):
        key = os.path.join('data/plugin/scalars/scalars', run, tag)
        return self._get_with_retry(key, lib.get_scalar, run, tag)

    @result()
    def scalars_list(self, run, tag, sub_tag):
        key = os.path.join('data/plugin/multiscalars/scalars', run, tag,
                           sub_tag)
        return self._get_with_retry(key, lib.get_scalars, run, tag, sub_tag)

    @result('text/csv')
    def scalar_data(self, run, tag, type='tsv'):
        key = os.path.join('data/plugin/scalars/data', run, tag, type)
        return self._get_with_retry(key, lib.get_scalar_data, run, tag, type)

    @result('text/csv')
    def scalars_data(self, run, tag, sub_tag, type='tsv'):
        key = os.path.join('data/plugin/multiscalars/data', run, tag, sub_tag,
                           type)
        return self._get_with_retry(key, lib.get_scalars_data, run, tag,
                                    sub_tag, type)

    @result()
    def image_list(self, mode, tag):
        key = os.path.join('data/plugin/images/images', mode, tag)
        return self._get_with_retry(key, lib.get_image_tag_steps, mode, tag)

    @result('image/png')
    def image_image(self, mode, tag, index=0):
        index = int(index)
        key = os.path.join('data/plugin/images/individualImage', mode, tag,
                           str(index))
        return self._get_with_retry(key, lib.get_individual_image, mode, tag,
                                    index)

    @result()
    def text_list(self, mode, tag):
        key = os.path.join('data/plugin/text/text', mode, tag)
        return self._get_with_retry(key, lib.get_text_tag_steps, mode, tag)

    @result('text/plain')
    def text_text(self, mode, tag, index=0):
        index = int(index)
        key = os.path.join('data/plugin/text/individualText', mode, tag,
                           str(index))
        return self._get_with_retry(key, lib.get_individual_text, mode, tag,
                                    index)

    @result()
    def audio_list(self, run, tag):
        key = os.path.join('data/plugin/audio/audio', run, tag)
        return self._get_with_retry(key, lib.get_audio_tag_steps, run, tag)

    @result('audio/wav')
    def audio_audio(self, run, tag, index=0):
        index = int(index)
        key = os.path.join('data/plugin/audio/individualAudio', run, tag,
                           str(index))
        return self._get_with_retry(key, lib.get_individual_audio, run, tag,
                                    index)

    @result()
    def embedding_embedding(self,
                            run,
                            tag='default',
                            reduction='pca',
                            dimension=2):
        dimension = int(dimension)
        key = os.path.join('data/plugin/embeddings/embeddings', run,
                           str(dimension), reduction)
        return self._get_with_retry(key, lib.get_embeddings, run, tag,
                                    reduction, dimension)

    @result()
    def embedding_list(self):
        return self._get_with_retry('data/plugin/embeddings/list',
                                    lib.get_embeddings_list)

    @result('text/tab-separated-values')
    def embedding_metadata(self, name):
        key = os.path.join('data/plugin/embeddings/metadata', name)
        return self._get_with_retry(key, lib.get_embedding_labels, name)

    @result('application/octet-stream')
    def embedding_tensor(self, name):
        key = os.path.join('data/plugin/embeddings/tensor', name)
        return self._get_with_retry(key, lib.get_embedding_tensors, name)

    @result()
    def histogram_tags(self):
        return self._get_with_retry('data/plugin/histogram/tags',
                                    lib.get_histogram_tags)

    @result()
    def histogram_list(self, run, tag):
        key = os.path.join('data/plugin/histogram/histogram', run, tag)
        return self._get_with_retry(key, lib.get_histogram, run, tag)

    @result()
    def pr_curves_pr_curve(self, run, tag):
        key = os.path.join('data/plugin/pr_curves/pr_curve', run, tag)
        return self._get_with_retry(key, lib.get_pr_curve, run, tag)

    @result()
    def roc_curves_roc_curve(self, run, tag):
        key = os.path.join('data/plugin/roc_curves/roc_curve', run, tag)
        return self._get_with_retry(key, lib.get_roc_curve, run, tag)

    @result()
    def pr_curves_steps(self, run):
        key = os.path.join('data/plugin/pr_curves/steps', run)
        return self._get_with_retry(key, lib.get_pr_curve_step, run)

    @result()
    def roc_curves_steps(self, run):
        key = os.path.join('data/plugin/roc_curves/steps', run)
        return self._get_with_retry(key, lib.get_roc_curve_step, run)

    @result('application/octet-stream', lambda s: {
        "Content-Disposition": 'attachment; filename="%s"' % s.model_name
    } if len(s.model_name) else None)
    def graph_static_graph(self):
        key = os.path.join('data/plugin/graphs/static_graph')
        return self._get_with_retry(key, lib.get_static_graph)

    @result()
    def graph_graph(self, run, expand_all, refresh):
        client_ip = request.remote_addr
        graph_reader = self.graph_reader_client_manager.get_data(client_ip)
        if expand_all is not None:
            if (expand_all.lower() == 'true'):
                expand_all = True
            else:
                expand_all = False
        else:
            expand_all = False
        if refresh is not None:
            if (refresh.lower() == 'true'):
                refresh = True
            else:
                refresh = False
        else:
            refresh = True
        return lib.get_graph(
            graph_reader, run, expand_all=expand_all, refresh=refresh)

    @result()
    def graph_upload(self):
        client_ip = request.remote_addr
        graph_reader = self.graph_reader_client_manager.get_data(client_ip)
        files = request.files
        if 'file' in files:
            file_handle = request.files['file']
            if 'pdmodel' in file_handle.filename:
                graph_reader.set_input_graph(file_handle.stream.read(),
                                             'pdmodel')
            elif 'vdlgraph' in file_handle.filename:
                graph_reader.set_input_graph(file_handle.stream.read(),
                                             'vdlgraph')

    @result()
    def graph_manipulate(self, run, nodeid, expand, keep_state):
        client_ip = request.remote_addr
        graph_reader = self.graph_reader_client_manager.get_data(client_ip)
        if expand is not None:
            if (expand.lower() == 'true'):
                expand = True
            else:
                expand = False
        else:
            expand = False
        if keep_state is not None:
            if (keep_state.lower() == 'true'):
                keep_state = True
            else:
                keep_state = False
        else:
            keep_state = False
        return lib.get_graph(graph_reader, run, nodeid, expand, keep_state)

    @result()
    def graph_search(self, run, nodeid, keep_state, is_node):
        client_ip = request.remote_addr
        graph_reader = self.graph_reader_client_manager.get_data(client_ip)
        if keep_state is not None:
            if (keep_state.lower() == 'true'):
                keep_state = True
            else:
                keep_state = False
        else:
            keep_state = False

        if is_node is not None:
            if (is_node.lower() == 'true'):
                is_node = True
            else:
                is_node = False
        else:
            is_node = False
        return lib.get_graph_search(graph_reader, run, nodeid, keep_state,
                                    is_node)

    @result()
    def graph_get_all_nodes(self, run):
        client_ip = request.remote_addr
        graph_reader = self.graph_reader_client_manager.get_data(client_ip)
        return lib.get_graph_all_nodes(graph_reader, run)


@result()
def get_component_tabs(*apis, vdl_args, request_args):
    '''
    Get component tabs in all apis, so tabs can be presented according to existed data in frontend.
    '''
    all_tabs = set()
    if vdl_args.component_tabs:
        return list(vdl_args.component_tabs)
    if vdl_args.logdir:
        for api in apis:
            all_tabs.update(api('component_tabs', request_args))
            all_tabs.add('static_graph')
    else:
        return [
            'static_graph', 'x2paddle', 'fastdeploy_server',
            'fastdeploy_client'
        ]
    return list(all_tabs)


def create_api_call(logdir, model, cache_timeout):
    api = Api(logdir, model, cache_timeout)
    routes = {
        'components': (api.components, []),
        'runs': (api.runs, []),
        'graph_runs': (api.graph_runs, []),
        'tags': (api.tags, []),
        'logs': (api.logs, []),
        'scalar/tags': (api.scalar_tags, []),
        'scalars/tags': (api.scalars_tags, []),
        'image/tags': (api.image_tags, []),
        'text/tags': (api.text_tags, []),
        'audio/tags': (api.audio_tags, []),
        'embedding/tags': (api.embedding_tags, []),
        'histogram/tags': (api.histogram_tags, []),
        'pr-curve/tags': (api.pr_curve_tags, []),
        'roc-curve/tags': (api.roc_curve_tags, []),
        'scalar/list': (api.scalar_list, ['run', 'tag']),
        'scalars/list': (api.scalars_list, ['run', 'tag', 'sub_tag']),
        'scalar/data': (api.scalar_data, ['run', 'tag', 'type']),
        'scalars/data': (api.scalars_data, ['run', 'tag', 'sub_tag', 'type']),
        'image/list': (api.image_list, ['run', 'tag']),
        'image/image': (api.image_image, ['run', 'tag', 'index']),
        'text/list': (api.text_list, ['run', 'tag']),
        'text/text': (api.text_text, ['run', 'tag', 'index']),
        'audio/list': (api.audio_list, ['run', 'tag']),
        'audio/audio': (api.audio_audio, ['run', 'tag', 'index']),
        'embedding/embedding': (api.embedding_embedding,
                                ['run', 'tag', 'reduction', 'dimension']),
        'embedding/list': (api.embedding_list, []),
        'embedding/tensor': (api.embedding_tensor, ['name']),
        'embedding/metadata': (api.embedding_metadata, ['name']),
        'histogram/list': (api.histogram_list, ['run', 'tag']),
        'graph/graph': (api.graph_graph, ['run', 'expand_all', 'refresh']),
        'graph/static_graph': (api.graph_static_graph, []),
        'graph/upload': (api.graph_upload, []),
        'graph/search': (api.graph_search,
                         ['run', 'nodeid', 'keep_state', 'is_node']),
        'graph/get_all_nodes': (api.graph_get_all_nodes, ['run']),
        'graph/manipulate': (api.graph_manipulate,
                             ['run', 'nodeid', 'expand', 'keep_state']),
        'pr-curve/list': (api.pr_curves_pr_curve, ['run', 'tag']),
        'roc-curve/list': (api.roc_curves_roc_curve, ['run', 'tag']),
        'pr-curve/steps': (api.pr_curves_steps, ['run']),
        'roc-curve/steps': (api.roc_curves_steps, ['run']),
        'hparams/importance': (api.hparam_importance, []),
        'hparams/data': (api.hparam_data, ['type']),
        'hparams/indicators': (api.hparam_indicator, []),
        'hparams/list': (api.hparam_list, []),
        'hparams/metric': (api.hparam_metric, ['run', 'metric']),
        'component_tabs': (api.component_tabs, [])
    }

    def call(path: str, args):
        route = routes.get(path)
        if not route:
            return json.dumps(gen_result(
                status=1, msg='api not found')), 'application/json', None
        method, call_arg_names = route
        call_args = [args.get(name) for name in call_arg_names]
        return method(*call_args)

    return call
