# Copyright 2018-2020 huajiweb Inc.
#
# 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.

"""huajiweb.

How to use huajiweb in 3 seconds:

    1. Write an app
    >>> import huajiweb as st
    >>> st.write(anything_you_want)

    2. Run your app
    $ huajiweb run my_script.py

    3. Use your app
    A new tab will open on your browser. That's your huajiweb app!

    4. Modify your code, save it, and watch changes live on your browser.

Take a look at the other commands in this module to find out what else
huajiweb can do:

    >>> dir(huajiweb)

Or try running our "Hello World":

    $ huajiweb hello

For more detailed info, see https://docs.huajiweb.io.
"""

# IMPORTANT: Prefix with an underscore anything that the user shouldn't see.

# NOTE: You'll see lots of "noqa: F821" in this file. That's because we
# manually mess with the local namespace so the linter can't know that some
# identifiers actually exist in the namespace.

# Must be at the top, to avoid circular dependency.
from huajiweb import logger as _logger
from huajiweb import config as _config

_LOGGER = _logger.get_logger("root")

# Give the package a version.
import pkg_resources as _pkg_resources
import uuid as _uuid
import subprocess
import platform
import os
from typing import Any, List, Tuple, Type

# This used to be pkg_resources.require('huajiweb') but it would cause
# pex files to fail. See #394 for more details.
__version__ = _pkg_resources.get_distribution("huajiweb").version

# Deterministic Unique huajiweb User ID
if (
    platform.system() == "Linux"
    and os.path.isfile("/etc/machine-id") == False
    and os.path.isfile("/var/lib/dbus/machine-id") == False
):
    print("Generate machine-id")
    subprocess.run(["sudo", "dbus-uuidgen", "--ensure"])

machine_id = str(_uuid.getnode())
if os.path.isfile("/etc/machine-id"):
    with open("/etc/machine-id", "r") as f:
        machine_id = f.read()
elif os.path.isfile("/var/lib/dbus/machine-id"):
    with open("/var/lib/dbus/machine-id", "r") as f:
        machine_id = f.read()

__installation_id__ = str(_uuid.uuid5(_uuid.NAMESPACE_DNS, machine_id))


import contextlib as _contextlib
import re as _re
import sys as _sys
import textwrap as _textwrap
import threading as _threading
import traceback as _traceback
import types as _types
import json as _json
import numpy as _np

from huajiweb import code_util as _code_util
from huajiweb import env_util as _env_util
from huajiweb import source_util as _source_util
from huajiweb import string_util as _string_util
from huajiweb import type_util as _type_util
from huajiweb.DeltaGenerator import DeltaGenerator as _DeltaGenerator
from huajiweb.ReportThread import add_report_ctx as _add_report_ctx
from huajiweb.ReportThread import get_report_ctx as _get_report_ctx
from huajiweb.errors import StreamlitAPIException
from huajiweb.proto import BlockPath_pb2 as _BlockPath_pb2
from huajiweb.util import functools_wraps as _functools_wraps

# Modules that the user should have access to. These are imported with "as"
# syntax pass mypy checking with implicit_reexport disabled.
from huajiweb.caching import cache as cache  # noqa: F401

# This is set to True inside cli._main_run(), and is False otherwise.
# If False, we should assume that DeltaGenerator functions are effectively
# no-ops, and adapt gracefully.
_is_running_with_streamlit = False


def _set_log_level():
    _logger.set_log_level(_config.get_option("global.logLevel").upper())
    _logger.init_tornado_logs()


# Make this file only depend on config option in an asynchronous manner. This
# avoids a race condition when another file (such as a test file) tries to pass
# in an alternative config.
_config.on_config_parsed(_set_log_level)


_main = _DeltaGenerator(container=_BlockPath_pb2.BlockPath.MAIN)
sidebar = _DeltaGenerator(container=_BlockPath_pb2.BlockPath.SIDEBAR)

# DeltaGenerator methods:

altair_chart = _main.altair_chart  # noqa: E221
area_chart = _main.area_chart  # noqa: E221
audio = _main.audio  # noqa: E221
balloons = _main.balloons  # noqa: E221
bar_chart = _main.bar_chart  # noqa: E221
bokeh_chart = _main.bokeh_chart  # noqa: E221
button = _main.button  # noqa: E221
checkbox = _main.checkbox  # noqa: E221
code = _main.code  # noqa: E221
dataframe = _main.dataframe  # noqa: E221
date_input = _main.date_input  # noqa: E221
deck_gl_chart = _main.deck_gl_chart  # noqa: E221
pydeck_chart = _main.pydeck_chart  # noqa: E221
empty = _main.empty  # noqa: E221
error = _main.error  # noqa: E221
exception = _main.exception  # noqa: E221
file_uploader = _main.file_uploader  # noqa: E221
graphviz_chart = _main.graphviz_chart  # noqa: E221
header = _main.header  # noqa: E221
help = _main.help  # noqa: E221
image = _main.image  # noqa: E221
info = _main.info  # noqa: E221
json = _main.json  # noqa: E221
latex = _main.latex  # noqa: E221
line_chart = _main.line_chart  # noqa: E221
map = _main.map  # noqa: E221
markdown = _main.markdown  # noqa: E221
multiselect = _main.multiselect  # noqa: E221
number_input = _main.number_input  # noqa: E221
plotly_chart = _main.plotly_chart  # noqa: E221
progress = _main.progress  # noqa: E221
pyplot = _main.pyplot  # noqa: E221
radio = _main.radio  # noqa: E221
selectbox = _main.selectbox  # noqa: E221
slider = _main.slider  # noqa: E221
subheader = _main.subheader  # noqa: E221
success = _main.success  # noqa: E221
table = _main.table  # noqa: E221
text = _main.text  # noqa: E221
text_area = _main.text_area  # noqa: E221
text_input = _main.text_input  # noqa: E221
time_input = _main.time_input  # noqa: E221
title = _main.title  # noqa: E221
vega_lite_chart = _main.vega_lite_chart  # noqa: E221
video = _main.video  # noqa: E221
warning = _main.warning  # noqa: E221

# Config

get_option = _config.get_option


def set_option(key, value):
    """Set config option.

    Currently, only two config options can be set within the script itself:
        * client.caching
        * client.displayEnabled

    Calling with any other options will raise StreamlitAPIException.

    Run `huajiweb config show` in the terminal to see all available options.

    Parameters
    ----------
    key : str
        The config option key of the form "section.optionName". To see all
        available options, run `huajiweb config show` on a terminal.

    value
        The new value to assign to this config option.

    """
    opt = _config._config_options[key]
    if opt.scriptable:
        _config.set_option(key, value)
        return

    raise StreamlitAPIException(
        "{key} cannot be set on the fly. Set as command line option, e.g. huajiweb run script.py --{key}, or in config.toml instead.".format(
            key=key
        )
    )


# Special methods:

_HELP_TYPES = (
    _types.BuiltinFunctionType,
    _types.BuiltinMethodType,
    _types.FunctionType,
    _types.MethodType,
    _types.ModuleType,
)  # type: Tuple[Type[Any], ...]


def write(*args, **kwargs):
    """Write arguments to the app.

    This is the swiss-army knife of huajiweb commands. It does different
    things depending on what you throw at it.

    Unlike other huajiweb commands, write() has some unique properties:

        1. You can pass in multiple arguments, all of which will be written.
        2. Its behavior depends on the input types as follows.
        3. It returns None, so it's "slot" in the App cannot be reused.

    Parameters
    ----------
    *args : any
        One or many objects to print to the App.

        Arguments are handled as follows:

            - write(string)     : Prints the formatted Markdown string, with
              support for LaTeX expression and emoji shortcodes.
              See docs for st.markdown for more.
            - write(data_frame) : Displays the DataFrame as a table.
            - write(error)      : Prints an exception specially.
            - write(func)       : Displays information about a function.
            - write(module)     : Displays information about the module.
            - write(dict)       : Displays dict in an interactive widget.
            - write(obj)        : The default is to print str(obj).
            - write(mpl_fig)    : Displays a Matplotlib figure.
            - write(altair)     : Displays an Altair chart.
            - write(keras)      : Displays a Keras model.
            - write(graphviz)   : Displays a Graphviz graph.
            - write(plotly_fig) : Displays a Plotly figure.
            - write(bokeh_fig)  : Displays a Bokeh figure.
            - write(sympy_expr) : Prints SymPy expression using LaTeX.

    unsafe_allow_html : bool
        This is a keyword-only argument that defaults to False.

        By default, any HTML tags found in strings will be escaped and
        therefore treated as pure text. This behavior may be turned off by
        setting this argument to True.

        That said, *we strongly advise* against it*. It is hard to write secure
        HTML, so by using this argument you may be compromising your users'
        security. For more information, see:

        https://github.com/huajiweb/huajiweb/issues/152

        *Also note that `unsafe_allow_html` is a temporary measure and may be
        removed from huajiweb at any time.*

        If you decide to turn on HTML anyway, we ask you to please tell us your
        exact use case here:

        https://discuss.huajiweb.io/t/96

        This will help us come up with safe APIs that allow you to do what you
        want.

    Example
    -------

    Its simplest use case is to draw Markdown-formatted text, whenever the
    input is a string:

    >>> write('Hello, *World!* :sunglasses:')

    .. output::
       https://share.huajiweb.io/0.50.2-ZWk9/index.html?id=Pn5sjhgNs4a8ZbiUoSTRxE
       height: 50px

    As mentioned earlier, `st.write()` also accepts other data formats, such as
    numbers, data frames, styled data frames, and assorted objects:

    >>> st.write(1234)
    >>> st.write(pd.DataFrame({
    ...     'first column': [1, 2, 3, 4],
    ...     'second column': [10, 20, 30, 40],
    ... }))

    .. output::
       https://share.huajiweb.io/0.25.0-2JkNY/index.html?id=FCp9AMJHwHRsWSiqMgUZGD
       height: 250px

    Finally, you can pass in multiple arguments to do things like:

    >>> st.write('1 + 1 = ', 2)
    >>> st.write('Below is a DataFrame:', data_frame, 'Above is a dataframe.')

    .. output::
       https://share.huajiweb.io/0.25.0-2JkNY/index.html?id=DHkcU72sxYcGarkFbf4kK1
       height: 300px

    Oh, one more thing: `st.write` accepts chart objects too! For example:

    >>> import pandas as pd
    >>> import numpy as np
    >>> import altair as alt
    >>>
    >>> df = pd.DataFrame(
    ...     np.random.randn(200, 3),
    ...     columns=['a', 'b', 'c'])
    ...
    >>> c = alt.Chart(df).mark_circle().encode(
    ...     x='a', y='b', size='c', color='c', tooltip=['a', 'b', 'c'])
    >>>
    >>> st.write(c)

    .. output::
       https://share.huajiweb.io/0.25.0-2JkNY/index.html?id=8jmmXR8iKoZGV4kXaKGYV5
       height: 200px

    """
    try:
        string_buffer = []  # type: List[str]
        unsafe_allow_html = kwargs.get("unsafe_allow_html", False)

        def flush_buffer():
            if string_buffer:
                markdown(
                    " ".join(string_buffer), unsafe_allow_html=unsafe_allow_html,
                )  # noqa: F821
                string_buffer[:] = []

        for arg in args:
            # Order matters!
            if isinstance(arg, str):
                string_buffer.append(arg)
            elif _type_util.is_dataframe_like(arg):
                flush_buffer()
                if len(_np.shape(arg)) > 2:
                    text(arg)
                else:
                    dataframe(arg)  # noqa: F821
            elif isinstance(arg, Exception):
                flush_buffer()
                exception(arg)  # noqa: F821
            elif isinstance(arg, _HELP_TYPES):
                flush_buffer()
                help(arg)
            elif _type_util.is_altair_chart(arg):
                flush_buffer()
                altair_chart(arg)
            elif _type_util.is_type(arg, "matplotlib.figure.Figure"):
                flush_buffer()
                pyplot(arg)
            elif _type_util.is_plotly_chart(arg):
                flush_buffer()
                plotly_chart(arg)
            elif _type_util.is_type(arg, "bokeh.plotting.figure.Figure"):
                flush_buffer()
                bokeh_chart(arg)
            elif _type_util.is_graphviz_chart(arg):
                flush_buffer()
                graphviz_chart(arg)
            elif _type_util.is_sympy_expession(arg):
                flush_buffer()
                latex(arg)
            elif _type_util.is_keras_model(arg):
                from tensorflow.python.keras.utils import vis_utils

                flush_buffer()
                dot = vis_utils.model_to_dot(arg)
                graphviz_chart(dot.to_string())
            elif isinstance(arg, (dict, list)):
                flush_buffer()
                json(arg)
            elif _type_util.is_namedtuple(arg):
                flush_buffer()
                json(_json.dumps(arg._asdict()))
            elif _type_util.is_pydeck(arg):
                flush_buffer()
                pydeck_chart(arg)
            else:
                string_buffer.append("`%s`" % str(arg).replace("`", "\\`"))

        flush_buffer()

    except Exception:
        _, exc, exc_tb = _sys.exc_info()
        exception(exc, exc_tb)  # noqa: F821


def _show(*args):
    """Write arguments and *argument names* to your app for debugging purposes.

    Show() has similar properties to write():

        1. You can pass in multiple arguments, all of which will be debugged.
        2. It returns None, so it's "slot" in the app cannot be reused.

    Parameters
    ----------
    *args : any
        One or many objects to debug in the App.

    Example
    -------

    >>> dataframe = pd.DataFrame({
    ...     'first column': [1, 2, 3, 4],
    ...     'second column': [10, 20, 30, 40],
    ... }))
    >>> st.show(dataframe)

    Notes
    -----

    This is an experimental feature with usage limitations:

    - The method must be called with the name `show`.
    - Must be called in one line of code, and only once per line.
    - When passing multiple arguments the inclusion of `,` or `)` in a string
    argument may cause an error.

    """
    if not args:
        return

    try:
        import inspect

        # Get the calling line of code
        current_frame = inspect.currentframe()
        if current_frame is None:
            warning("`show` not enabled in the shell")
            return
        lines = inspect.getframeinfo(current_frame.f_back)[3]

        if not lines:
            warning("`show` not enabled in the shell")
            return

        # Parse arguments from the line
        line = lines[0].split("show", 1)[1]
        inputs = _code_util.get_method_args_from_code(args, line)

        # Escape markdown and add deltas
        for idx, input in enumerate(inputs):
            escaped = _string_util.escape_markdown(input)

            markdown("**%s**" % escaped)
            write(args[idx])

    except Exception:
        _, exc, exc_tb = _sys.exc_info()
        exception(exc, exc_tb)  # noqa: F821


@_contextlib.contextmanager
def spinner(text="In progress..."):
    """Temporarily displays a message while executing a block of code.

    Parameters
    ----------
    text : str
        A message to display while executing that block

    Example
    -------

    >>> with st.spinner('Wait for it...'):
    >>>     time.sleep(5)
    >>> st.success('Done!')

    """
    import huajiweb.caching as caching

    # @st.cache optionally uses spinner for long-running computations.
    # Normally, huajiweb warns the user when they call st functions
    # from within an @st.cache'd function. But we do *not* want to show
    # these warnings for spinner's message, so we create and mutate this
    # message delta within the "suppress_cached_st_function_warning"
    # context.
    with caching.suppress_cached_st_function_warning():
        message = empty()

    try:
        # Set the message 0.1 seconds in the future to avoid annoying
        # flickering if this spinner runs too quickly.
        DELAY_SECS = 0.1
        display_message = True
        display_message_lock = _threading.Lock()

        def set_message():
            with display_message_lock:
                if display_message:
                    with caching.suppress_cached_st_function_warning():
                        message.warning(str(text))

        _add_report_ctx(_threading.Timer(DELAY_SECS, set_message)).start()

        # Yield control back to the context.
        yield
    finally:
        if display_message_lock:
            with display_message_lock:
                display_message = False
        with caching.suppress_cached_st_function_warning():
            message.empty()


_SPACES_RE = _re.compile("\\s*")


@_contextlib.contextmanager
def echo():
    """Use in a `with` block to draw some code on the app, then execute it.

    Example
    -------

    >>> with st.echo():
    >>>     st.write('This code will be printed')

    """
    code = empty()  # noqa: F821
    try:
        frame = _traceback.extract_stack()[-3]
        filename, start_line = frame.filename, frame.lineno
        yield
        frame = _traceback.extract_stack()[-3]
        end_line = frame.lineno
        lines_to_display = []  # type: List[str]
        with _source_util.open_python_file(filename) as source_file:
            source_lines = source_file.readlines()
            lines_to_display.extend(source_lines[start_line:end_line])
            match = _SPACES_RE.match(lines_to_display[0])
            initial_spaces = match.end() if match else 0
            for line in source_lines[end_line:]:
                match = _SPACES_RE.match(line)
                indentation = match.end() if match else 0
                # The != 1 is because we want to allow '\n' between sections.
                if indentation != 1 and indentation < initial_spaces:
                    break
                lines_to_display.append(line)
        line_to_display = _textwrap.dedent("".join(lines_to_display))
        code.code(line_to_display, "python")

    except FileNotFoundError as err:
        code.warning("Unable to display code. %s" % err)


def _transparent_write(*args):
    """This is just st.write, but returns the arguments you passed to it."""
    write(*args)
    if len(args) == 1:
        return args[0]
    return args


# We want to show a warning when the user runs a huajiweb script without
# 'huajiweb run', but we need to make sure the warning appears only once no
# matter how many times __init__ gets loaded.
_repl_warning_has_been_displayed = False


def _maybe_print_repl_warning():
    global _repl_warning_has_been_displayed

    if not _repl_warning_has_been_displayed:
        _repl_warning_has_been_displayed = True

        if _env_util.is_repl():
            _LOGGER.warning(
                _textwrap.dedent(
                    """

                Will not generate huajiweb app

                  To generate an app, use huajiweb in a file and run it with:
                  $ huajiweb run [FILE_NAME] [ARGUMENTS]

                """
                )
            )

        elif _config.get_option("global.showWarningOnDirectExecution"):
            script_name = _sys.argv[0]

            _LOGGER.warning(
                _textwrap.dedent(
                    """

                Will not generate huajiweb App

                  To generate an App, run this file with:
                  $ huajiweb run %s [ARGUMENTS]

                """
                ),
                script_name,
            )


# Feature namespaces


class _BetaNamespace(object):
    """This namespace holds huajiweb features that are in currently in beta.

    "Beta features" are features that are planned to become part of the
    huajiweb core but whose APIs/behavior have not stabilized yet. They may
    change in ways that are not backward-compatible.

    The lifecycle of a beta feature is:

    1. Feature is added to the beta namespace.
    2. After a few months: the feature's API stabilizes and the feature is
       *cloned* into the `st` namespace, so it will exist in both `st` and
       `st.beta`. At this point, users will see a warning when using the
       version of the feature that lives in the beta namespace -- but the
       `st.beta` feature will otherwise still work.
    3. After 3 months: the feature is *removed* from the `st.beta` namespace,
       but there will still be a stub in `st.beta` that shows an error with
       appropriate instructions.
    4. After 3 months: the stub in `st.beta` is removed.

    The main difference between `st.beta` and `st.experimental` is that beta
    features will always make it into the `st` namespace at some point, while
    experimental ones often do not.

    """

    # Add beta features here. For example:
    # foo = _foo
    pass


class _ExperimentalNamespace(object):
    """This namespace holds highly experimental huajiweb features.

    "Experimental features" are features that we are still trying to
    understand.  If they are successful, they will at some point become part of
    core huajiweb, by moving to the `st.beta` namespace and then core `st`. If
    unsuccessful, they may be removed.

    Why this exists: we used to provide several Github Gists that allowed
    people to hack experimental features into huajiweb, but it was hard to
    guarantee that the Gists always worked when a new version of huajiweb came
    out. Our solution is to move all that code out of Gists and into
    `st.experimental` so we can run tests against them and provide at least
    *some* forward-compatibility.

    Note, however, that the API for experimental features may change at any
    point!

    The lifecycle of an experimental feature is:

    1. Feature is added to the experimental namespace.
    2. Feature is possibly tweaked over time, with possible API/behavior
       breakages.
    3. At some point, we either move the feature into `st.beta` or remove it
       from `st.experimental`. Either way, we leave a stub in `st.experimental`
       that shows an error with appropriate instructions.

    The main difference between `st.experimental` and `st.beta` is that beta
    features will always make it into the `st` namespace at some point, while
    experimental ones often do not.
    """

    # Add experimental features here. For example:
    # foo = _foo
    show = _show


beta = _BetaNamespace
experimental = _ExperimentalNamespace
