#-----------------------------------------------------------------------------
# Copyright (c) 2012 - 2021, Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
import re
from pathlib import Path
from typing import Callable, List, Union

# External imports
from channels.http import AsgiHandler
from django.conf.urls import url
from django.urls.resolvers import URLPattern

# Bokeh imports
from bokeh.application import Application
from bokeh.application.handlers.document_lifecycle import DocumentLifecycleHandler
from bokeh.application.handlers.function import FunctionHandler
from bokeh.command.util import build_single_handler_application, build_single_handler_applications
from bokeh.server.contexts import ApplicationContext

# Bokeh imports
from .consumers import AutoloadJsConsumer, DocConsumer, WSConsumer

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

__all__ = (
    'RoutingConfiguration',
)

ApplicationLike = Union[Application, Callable, Path]

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

class Routing:
    url: str
    app: Application
    app_context: ApplicationContext
    document: bool
    autoload: bool

    def __init__(self, url: str, app: ApplicationLike, *, document: bool = False, autoload: bool = False) -> None:
        self.url = url
        self.app = self._fixup(self._normalize(app))
        self.app_context = ApplicationContext(self.app, url=self.url)
        self.document = document
        self.autoload = autoload

    def _normalize(self, obj: ApplicationLike) -> Application:
        if callable(obj):
            return Application(FunctionHandler(obj, trap_exceptions=True))
        elif isinstance(obj, Path):
            return build_single_handler_application(obj)
        else:
            return obj

    def _fixup(self, app: Application) -> Application:
        if not any(isinstance(handler, DocumentLifecycleHandler) for handler in app.handlers):
            app.add(DocumentLifecycleHandler())
        return app

def document(url: str, app: ApplicationLike) -> Routing:
    return Routing(url, app, document=True)

def autoload(url: str, app: ApplicationLike) -> Routing:
    return Routing(url, app, autoload=True)

def directory(*apps_paths: Path) -> List[Routing]:
    paths: List[Path] = []

    for apps_path in apps_paths:
        if apps_path.exists():
            paths += [ entry for entry in apps_path.glob("*") if is_bokeh_app(entry) ]
        else:
            log.warn(f"bokeh applications directory '{apps_path}' doesn't exist")

    return [ document(url, app) for url, app in build_single_handler_applications(paths).items() ]


class RoutingConfiguration:
    _http_urlpatterns: List[str] = []
    _websocket_urlpatterns: List[str] = []

    def __init__(self, routings: List[Routing]) -> None:
        for routing in routings:
            self._add_new_routing(routing)

    def get_http_urlpatterns(self) -> List[URLPattern]:
        return self._http_urlpatterns + [url(r"", AsgiHandler)]

    def get_websocket_urlpatterns(self) -> List[URLPattern]:
        return self._websocket_urlpatterns

    def _add_new_routing(self, routing: Routing) -> None:
        kwargs = dict(app_context=routing.app_context)

        def join(*components):
            return "/".join([ component.strip("/") for component in components if component ])

        def urlpattern(suffix=""):
            return r"^{}$".format(join(re.escape(routing.url)) + suffix)

        if routing.document:
            self._http_urlpatterns.append(url(urlpattern(), DocConsumer, kwargs=kwargs))
        if routing.autoload:
            self._http_urlpatterns.append(url(urlpattern("/autoload.js"), AutoloadJsConsumer, kwargs=kwargs))

        self._websocket_urlpatterns.append(url(urlpattern("/ws"), WSConsumer, kwargs=kwargs))

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------

def is_bokeh_app(entry: Path) -> bool:
    return (entry.is_dir() or entry.name.endswith(('.py', '.ipynb'))) and not entry.name.startswith((".", "_"))

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
