import os
import numpy as np
import torch
import os.path as osp
import torch.nn as nn
import torch.nn.functional as F
class TemporalEncoder(nn.Module):
    def __init__(
            self,
            n_layers=1,
            hidden_size=2048,
            add_linear=False,
            bidirectional=False,
            use_residual=True
    ):
        super(TemporalEncoder, self).__init__()

        self.gru = nn.GRU(
            input_size=2048,
            hidden_size=hidden_size,
            bidirectional=bidirectional,
            num_layers=n_layers
        )

        self.linear = None
        if bidirectional:
            self.linear = nn.Linear(hidden_size*2, 2048)
        elif add_linear:
            self.linear = nn.Linear(hidden_size, 2048)
        self.use_residual = use_residual

    def forward(self, x):
        n,t,f = x.shape
        x = x.permute(1,0,2) # NTF -> TNF
        y, _ = self.gru(x)
        if self.linear:
            y = F.relu(y)
            y = self.linear(y.view(-1, y.size(-1)))
            y = y.view(t,n,f)
        if self.use_residual and y.shape[-1] == 2048:
            y = y + x
        y = y.permute(1,0,2) # TNF -> NTF
        return y

class VIBEX(nn.Module):
    def __init__(
            self,
            seqlen,
            mean_param,
            batch_size=64,
            n_layers=1,
            hidden_size=2048,
            add_linear=False,
            bidirectional=False,
            use_residual=True, device='cuda',
    ):

        super(VIBEX, self).__init__()

        self.seqlen = seqlen
        self.batch_size = batch_size

        self.encoder = TemporalEncoder(
            n_layers=n_layers,
            hidden_size=hidden_size,
            bidirectional=bidirectional,
            add_linear=add_linear,
            use_residual=use_residual,
        )
        self.regressor = SMPLXRegressor(seqlen, mean_param)
        # regressor can predict cam, pose and shape params in an iterative way
        # self.regressor = Regressor()

    def forward(self, input, J_regressor=None):
        # input size NTF
        batch_size, seqlen = input.shape[:2]

        features = self.encoder(input)
        features = features.reshape(-1, features.size(-1))

        smplx_output = self.regressor(features)
        # smpl_output = self.regressor(feature, J_regressor=J_regressor)
        # for s in smpl_output:
        #     s['theta'] = s['theta'].reshape(batch_size, seqlen, -1)
        #     s['verts'] = s['verts'].reshape(batch_size, seqlen, -1, 3)
        #     s['kp_2d'] = s['kp_2d'].reshape(batch_size, seqlen, -1, 2)
        #     s['kp_3d'] = s['kp_3d'].reshape(batch_size, seqlen, -1, 3)
        #     s['rotmat'] = s['rotmat'].reshape(batch_size, seqlen, -1, 3, 3)

        return smplx_output
class SMPLXRegressor(nn.Module):
    def __init__(self, seqlen, mean_params=None):
        super(SMPLXRegressor, self).__init__()
        self.seqlen = seqlen
        self.mean_params = mean_params
        self.fc1 = nn.Linear(512 * 4 + mean_params.shape[1], 1024)
        self.drop1 = nn.Dropout()
        self.fc2 = nn.Linear(1024, 1024)
        self.drop2 = nn.Dropout()

        #--Predict pose (318), shape (10), expression (10), camera (3)
        self.decpose = nn.Linear(1024, 318)
        self.decshape = nn.Linear(1024, 10)
        self.decexp = nn.Linear(1024, 10)
        self.deccam = nn.Linear(1024, 3)

        nn.init.xavier_uniform_(self.decpose.weight, gain=0.01)
        nn.init.xavier_uniform_(self.decshape.weight, gain=0.01)
        nn.init.xavier_uniform_(self.decexp.weight, gain=0.01)
        nn.init.xavier_uniform_(self.deccam.weight, gain=0.01)

    def forward(self, x, init_pose=None, init_shape=None, init_cam=None, n_iter=3, J_regressor=None):
        batch_size = x.shape[0]
        _init = self.mean_params.expand(batch_size, -1)

        pred_pose = _init[:, :318]
        pred_shape = _init[:, 318: 328]
        pred_exp = _init[:, 328: 338]
        pred_cam = _init[:, 338: 341]
        for i in range(n_iter):
            xc = torch.cat([x, pred_pose, pred_shape, pred_exp, pred_cam],1)
            xc = self.fc1(xc)
            xc = self.drop1(xc)
            xc = self.fc2(xc)
            xc = self.drop2(xc)

            pred_pose = self.decpose(xc) + pred_pose
            pred_shape = self.decshape(xc) + pred_shape
            pred_exp = self.decexp(xc) + pred_exp
            pred_cam = self.deccam(xc) + pred_cam
            pred_param = torch.cat([pred_pose, pred_shape, pred_shape, pred_exp, pred_cam], 1)

        return pred_param

class Regressor(nn.Module):
    def __init__(self, smpl_mean_params=''):
        super(Regressor, self).__init__()

        npose = 24 * 6

        self.fc1 = nn.Linear(512 * 4 + npose + 13, 1024)
        self.drop1 = nn.Dropout()
        self.fc2 = nn.Linear(1024, 1024)
        self.drop2 = nn.Dropout()
        self.decpose = nn.Linear(1024, npose)
        self.decshape = nn.Linear(1024, 10)
        self.deccam = nn.Linear(1024, 3)
        nn.init.xavier_uniform_(self.decpose.weight, gain=0.01)
        nn.init.xavier_uniform_(self.decshape.weight, gain=0.01)
        nn.init.xavier_uniform_(self.deccam.weight, gain=0.01)

        self.smpl = SMPL(
            SMPL_MODEL_DIR,
            batch_size=64,
            create_transl=False
        )

        mean_params = np.load(smpl_mean_params)
        init_pose = torch.from_numpy(mean_params['pose'][:]).unsqueeze(0)
        init_shape = torch.from_numpy(mean_params['shape'][:].astype('float32')).unsqueeze(0)
        init_cam = torch.from_numpy(mean_params['cam']).unsqueeze(0)
        self.register_buffer('init_pose', init_pose)
        self.register_buffer('init_shape', init_shape)
        self.register_buffer('init_cam', init_cam)

    def forward(self, x, init_pose=None, init_shape=None, init_cam=None, n_iter=3, J_regressor=None):
        batch_size = x.shape[0]

        if init_pose is None:
            init_pose = self.init_pose.expand(batch_size, -1)
        if init_shape is None:
            init_shape = self.init_shape.expand(batch_size, -1)
        if init_cam is None:
            init_cam = self.init_cam.expand(batch_size, -1)

        pred_pose = init_pose
        pred_shape = init_shape
        pred_cam = init_cam
        for i in range(n_iter):
            xc = torch.cat([x, pred_pose, pred_shape, pred_cam], 1)
            xc = self.fc1(xc)
            xc = self.drop1(xc)
            xc = self.fc2(xc)
            xc = self.drop2(xc)
            pred_pose = self.decpose(xc) + pred_pose
            pred_shape = self.decshape(xc) + pred_shape
            pred_cam = self.deccam(xc) + pred_cam

        # pred_rotmat = rot6d_to_rotmat(pred_pose).view(batch_size, 24, 3, 3)

        pred_output = self.smpl(
            betas=pred_shape,
            body_pose=pred_rotmat[:, 1:],
            global_orient=pred_rotmat[:, 0].unsqueeze(1),
            pose2rot=False
        )

        pred_vertices = pred_output.vertices
        pred_joints = pred_output.joints

        if J_regressor is not None:
            J_regressor_batch = J_regressor[None, :].expand(pred_vertices.shape[0], -1, -1).to(pred_vertices.device)
            pred_joints = torch.matmul(J_regressor_batch, pred_vertices)
            pred_joints = pred_joints[:, H36M_TO_J14, :]

        pred_keypoints_2d = projection(pred_joints, pred_cam)

        pose = rotation_matrix_to_angle_axis(pred_rotmat.reshape(-1, 3, 3)).reshape(-1, 72)

        output = [{
            'theta'  : torch.cat([pred_cam, pose, pred_shape], dim=1),
            'verts'  : pred_vertices,
            'kp_2d'  : pred_keypoints_2d,
            'kp_3d'  : pred_joints,
            'rotmat' : pred_rotmat
        }]
        return output


from django.shortcuts import get_object_or_404, render, redirect
from django.template.loader import render_to_string
from django.http import HttpResponseRedirect, JsonResponse
from django.urls import reverse_lazy

from django.views.generic import TemplateView, ListView, DetailView, CreateView, View

from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages

from .models import Article, Review
from quizes.models import Quiz
from .service import (
        add_like_to_the_article,
        add_dislike_to_the_article,
        add_like_to_the_article_review,
        add_dislike_to_the_article_review,
)

from .forms import ReviewForm, ReplyToReviewForm

class ArticlesListView(ListView):
    model = Article
    template_name = 'articles/view_articles.html'
    context_object_name = 'articles'

class SearchArticle(View):
    def get(self, request):
        name_of_necessary_article = request.GET.get('search')
        articles = Article.objects.filter(title__icontains=name_of_necessary_article)
        if articles:
            return render(request, 'articles/view_articles.html', {'articles':articles})

        messages.error(request, 'Нет подходящих результатов.')
        return HttpResponseRedirect(reverse_lazy('articles:view-articles'))

class ViewArticle(DetailView):
    model = Article
    template_name = 'articles/view_article.html'
    context_object_name = 'article'
    slug_field = 'slug'
    slug_url_kwarg = 'article_slug'

    def get_context_data(self, **kwargs):
        """Adding the Review form and Reviews"""
        context = super().get_context_data(**kwargs)
        article = context.get('article')

        # Adding article reviews
        reviews = Review.objects.filter(article=article).order_by('-date_of_creating')
        context['reviews'] = reviews

        quizes = Quiz.objects.get()

        # Adding review form
        form = ReviewForm()
        reply_form = ReplyToReviewForm()
        context['form'] = form
        context['reply_form'] = reply_form
        return context

class ViewQuizToArticle(LoginRequiredMixin, View):
    def post(self, request, template_name='quizes/create_quiz.html'):
        form = QuizForm(request.POST)

        if form.is_valid():
            try:
                context = get_questions(request, form)
            except ValueError as e:
                messages.error(self.request, e)
                return render(request, template_name, {'form': form})
            return render(request, template_name='quizes/created_quiz.html', context=context)
        return render(request, template_name, {'form': form})

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        return context

class AddLikeToArticle(LoginRequiredMixin, View):
    def post(self, request, article_slug, *args, **kwargs):
        article = get_object_or_404(Article, slug=article_slug)
        add_like_to_the_article(request, article)
        return redirect(reverse_lazy('articles:view-article', args=[str(article.slug)]) + f'#likes_and_dislikes_for_article_{article.slug}')

class AddDislikeToArticle(LoginRequiredMixin, View):
    def post(self, request, article_slug, *args, **kwags):
        article = get_object_or_404(Article, slug=article_slug)
        add_dislike_to_the_article(request, article)
        return redirect(reverse_lazy('articles:view-article', args=[str(article.slug)]) + f'#likes_and_dislikes_for_article_{article.slug}')

class AddReviewToArticle(LoginRequiredMixin, CreateView):
    model = Review
    form_class = ReviewForm

    def post(self, request, article_slug):
        article = get_object_or_404(Article, slug=article_slug)
        user = request.user

        form = ReviewForm(request.POST)
        if form.is_valid():
            review = form.save(commit=False)
            review.article = article
            review.user = user
            review.save()

        return HttpResponseRedirect(reverse_lazy('articles:view-article', args=[str(article.slug)])+ f'#comment_{review.pk}')

class ReplyToReview(LoginRequiredMixin, CreateView):
    model = Review
    form_class = ReplyToReviewForm

    def post(self, request, article_slug, review_pk):
        article = get_object_or_404(Article, slug=article_slug)
        parent_review = get_object_or_404(Review, pk=review_pk)
        user = request.user

        form = ReplyToReviewForm(request.POST)
        if form.is_valid():
            review = form.save(commit=False)
            review.article = article
            review.user = user
            review.parent = parent_review
            review.save()
        return redirect(reverse_lazy('articles:view-article', args=[str(article.slug)]) + f'#comment_{review.pk}')

class AddLikeToReview(LoginRequiredMixin, View):
    def post(self, request, article_slug, review_pk, *args, **kwargs):
        article = get_object_or_404(Article, slug=article_slug)
        review = get_object_or_404(Review, pk=review_pk)
        add_like_to_the_article_review(request, review)
        return redirect(reverse_lazy('articles:view-article', args=[str(article.slug)]) + f'#comment_{review.pk}')

class AddDislikeToReview(LoginRequiredMixin, View):
    def post(self, request, article_slug, review_pk):
        article = get_object_or_404(Article, slug=article_slug)
        review = get_object_or_404(Review, pk=review_pk)
        add_dislike_to_the_article_review(request, review)
        return redirect(reverse_lazy('articles:view-article', args=[str(article.slug)]) + f'#comment_{review.pk}')


import weaviate

client = weaviate.Client(
    url="http://localhost:8080",
)

# The idea is copy the full intermediateText so I don't have to make
# Intermediates themselves publishable
object = {
    "class": "Annotation",
    "vectorizer": "none",
    "vectorIndexConfig": {
        "skip": "true"
    },
    "properties": [
        {
            "name": "reportHighlightText",
            "dataType": ["text"],
        },
        {
            "name": "intermediateHighlightText",
            "dataType": ["text"],
        },
        {
            "name": "intermediateText",
            "dataType": ["text"],
        },
        {
            "name": "intermediate",
            "dataType": ["Intermediate"],
        },
        {
            "name": "report",
            "dataType": ["Report"],
        },
    ],
}

client.schema.create_class(object)

add_prop = {
    "name": "annotations",
    "dataType": ["Annotation"]
}
client.schema.property.create("Report", add_prop)

add_prop = {
    "name": "annotations",
    "dataType": ["Annotation"]
}
client.schema.property.create("Intermediate", add_prop)


import logging
from typing import Callable
from typing import List

import geopandas as gp
import knime.types.geospatial as gt
import knime_extension as knext

from shapely.geometry import Point
from shapely.geometry import LineString
from shapely.geometry import Polygon
from shapely.geometry import MultiPoint
from shapely.geometry import MultiLineString
from shapely.geometry import MultiPolygon
from shapely.geometry import GeometryCollection

LOGGER = logging.getLogger(__name__)

############################################
# Geometry value types
############################################

TYPE_GEO = knext.logical(gt.GeoValue)

TYPE_POINT = knext.logical(Point)
TYPE_LINE = knext.logical(LineString)
TYPE_POLYGON = knext.logical(Polygon)

TYPE_MULTI_POINT = knext.logical(MultiPoint)
TYPE_MULTI_LINE = knext.logical(MultiLineString)
TYPE_MULTI_POLYGON = knext.logical(MultiPolygon)

TYPE_GEO_COLLECTION = knext.logical(GeometryCollection)

############################################
# Column selection helper
############################################

__DEF_GEO_COL_LABEL = "Geometry column"
__DEF_GEO_COL_DESC = "Select the geometry column to use."

__CELL_TYPE_GEO = "org.knime.geospatial.core.data.cell.Geo"
__CELL_TYPE_POINT = "GeoPointCell"
__CELL_TYPE_LINE = "GeoLineCell"
__CELL_TYPE_POLYGON = "GeoPolygonCell"
__CELL_TYPE_COLLECTION = "GeoCollectionCell"
__CELL_TYPE_MULTI_POINT = "GeoMultiPointCell"
__CELL_TYPE_MULTI_LINE = "GeoMultiLineCell"
__CELL_TYPE_MULTI_POLYGON = "GeoMultiPolygonCell"

# The default request header to use in all nodes that perform a web request
WEB_REQUEST_HEADER = {"User-Agent": "KNIME-Geospatial/1.1"}

def geo_point_col_parameter(
    label: str = __DEF_GEO_COL_LABEL,
    description: str = __DEF_GEO_COL_DESC,
) -> knext.ColumnParameter:
    """
    Returns a column selection parameter that only supports GeoPoint columns.
    @return: Column selection parameter for point columns
    """
    return typed_geo_col_parameter(label, description, is_geo_point)

def geo_col_parameter(
    label: str = __DEF_GEO_COL_LABEL,
    description: str = __DEF_GEO_COL_DESC,
) -> knext.ColumnParameter:
    """
    Returns a column selection parameter that supports Geo columns.
    @return: Column selection parameter for all Geo columns
    """
    return typed_geo_col_parameter(label, description, is_geo)

def typed_geo_col_parameter(
    label: str = __DEF_GEO_COL_LABEL,
    description: str = __DEF_GEO_COL_DESC,
    type_filter: Callable[[knext.Column], bool] = None,
) -> knext.ColumnParameter:
    """
    Returns a column selection parameter that allows the user to select all columns that are compatible with
    the provided type filter.
    @return: Column selection parameter
    """
    return knext.ColumnParameter(
        label=label,
        description=description,
        column_filter=type_filter,
        include_row_key=False,
        include_none_column=False,
    )

def negate(function):
    """
    Negates the incoming function e.g. negate(is_numeric) can be used in a column parameter to allow the user
    to select from all none numeric columns.
    @return: the negated input function e.g. if the input function returns true this function returns false
    """

    def new_function(*args, **kwargs):
        return not function(*args, **kwargs)

    return new_function

def boolean_or(*functions):
    """
    Return True if any of the given functions returns True
    @return: True if any of the functions returns True
    """

    def new_function(*args, **kwargs):
        return any(f(*args, **kwargs) for f in functions)

    return new_function

def boolean_and(*functions):
    """
    Return True if all of the given functions return True
    @return: True if all of the functions return True
    """

    def new_function(*args, **kwargs):
        return all(f(*args, **kwargs) for f in functions)

    return new_function

def is_numeric(column: knext.Column) -> bool:
    """
    Checks if column is numeric e.g. int, long or double.
    @return: True if Column is numeric
    """
    return (
        column.ktype == knext.double()
        or column.ktype == knext.int32()
        or column.ktype == knext.int64()
    )

def is_int(column: knext.Column) -> bool:
    """
    Checks if column is integer (int32 only).
    @return: True if Column is integer
    """
    return column.ktype == knext.int32()

def is_long(column: knext.Column) -> bool:
    """
    Checks if column is long (e.g. int32 or int64).
    @return: True if Column is long
    """
    return column.ktype in [
        knext.int32(),
        knext.int64(),
    ]

def is_string(column: knext.Column) -> bool:
    """
    Checks if column is string
    @return: True if Column is string
    """
    return column.ktype == knext.string()

def is_boolean(column: knext.Column) -> bool:
    """
    Checks if column is boolean
    @return: True if Column is boolean
    """
    return column.ktype == knext.boolean()

def is_numeric_or_string(column: knext.Column) -> bool:
    """
    Checks if column is numeric or string
    @return: True if Column is numeric or string
    """
    return boolean_or(is_numeric, is_string)(column)

def is_int_or_string(column: knext.Column) -> bool:
    """
    Checks if column is int or string
    @return: True if Column is numeric or string
    """
    return column.ktype in [
        knext.int32(),
        knext.int64(),
        knext.string(),
    ]

def is_binary(column: knext.Column) -> bool:
    """
    Checks if column is binary
    @return: True if Column is binary
    """
    return column.ktype == knext.blob

def is_date(column: knext.Column) -> bool:
    """
    Checks if column is compatible to the GeoValue interface and thus returns true for all geospatial types such as:
    GeoPointCell, GeoLineCell, GeoPolygonCell, GeoMultiPointCell, GeoMultiLineCell, GeoMultiPolygonCell, ...
    @return: True if Column Type is GeoValue compatible
    """
    return __is_type_x(column, "org.knime.core.data.v2.time.LocalDateValueFactory")

def is_geo(column: knext.Column) -> bool:
    """
    Checks if column is compatible to the GeoValue interface and thus returns true for all geospatial types such as:
    GeoPointCell, GeoLineCell, GeoPolygonCell, GeoMultiPointCell, GeoMultiLineCell, GeoMultiPolygonCell, ...
    @return: True if Column Type is GeoValue compatible
    """
    return __is_type_x(column, __CELL_TYPE_GEO)

def is_geo_point(column: knext.Column) -> bool:
    """
    Checks if column is a GeoPointCell.
    @return: True if Column Type is a GeoPointCell
    """
    return __is_type_x(column, __CELL_TYPE_POINT)

def is_geo_line(column: knext.Column) -> bool:
    """
    Checks if column is a GeoLineCell.
    @return: True if Column Type is a GeoLineCell
    """
    return __is_type_x(column, __CELL_TYPE_LINE)

def is_geo_polygon(column: knext.Column) -> bool:
    """
    Checks if column is a GeoPolygonCell.
    @return: True if Column Type is a GeoPolygonCell
    """
    return __is_type_x(column, __CELL_TYPE_POLYGON)

def is_geo_collection(column: knext.Column) -> bool:
    """
    Checks if column is a GeoCollectionCell.
    @return: True if Column Type is a GeoCollectionCell
    """
    return __is_type_x(column, __CELL_TYPE_COLLECTION)

def is_geo_multi_point(column: knext.Column) -> bool:
    """
    Checks if column is a GeoMultiPointCell.
    @return: True if Column Type is a GeoMultiPointCell
    """
    return __is_type_x(column, __CELL_TYPE_MULTI_POINT)

def is_geo_multi_line(column: knext.Column) -> bool:
    """
    Checks if column is a GeoMultiLineCell.
    @return: True if Column Type is a GeoMultiLineCell
    """
    return __is_type_x(column, __CELL_TYPE_MULTI_LINE)

def is_geo_multi_polygon(column: knext.Column) -> bool:
    """
    Checks if column is a GeoMultiPolygonCell.
    @return: True if Column Type is a GeoMultiPolygonCell
    """
    return __is_type_x(column, __CELL_TYPE_MULTI_POLYGON)

def __is_type_x(column: knext.Column, type: str) -> bool:
    """
    Checks if column contains the given type whereas type can be :
    GeoPointCell, GeoLineCell, GeoPolygonCell, GeoMultiPointCell, GeoMultiLineCell, GeoMultiPolygonCell, ...
    @return: True if Column Type is a GeoLogical Point
    """
    return (
        isinstance(column.ktype, knext.LogicalType)
        and type in column.ktype.logical_type
    )

############################################
# GeoPandas node class decorator
############################################
def geo_node_description(short_description: str, description: str, references: dict):
    """This decorator takes the provided information and generates a standardized node description
    for nodes that are based on GeoPandas functionality."""

    def set_description(node_factory):
        s = f"{short_description}\n"
        s += f"{description}\n\n"
        # s += "___\n\n"  # separator line between description and general part
        s += "The node is based on the [GeoPandas](https://geopandas.org/) project and uses the following related information and function"
        if references is not None:
            if len(references) > 1:
                s += "s"
            s += ":"
            s += "\n\n"
            for key in references:
                s += f"- [{key}]({references[key]})\n"
        node_factory.__doc__ = s
        return node_factory

    return set_description

def census_node_description(short_description: str, description: str, references: dict):
    """This decorator takes the provided information and generates a standardized node description
    for nodes that are based on US Census data."""

    def set_description(node_factory):
        s = f"{short_description}\n"
        s += f"{description}\n\n"
        # s += "___\n\n"  # separator line between description and general part
        s += "The node is based on the data from [US Census](https://www.census.gov/) and "
        s += "[FIPS code](https://www.census.gov/library/reference/code-lists/ansi.html) "
        s += "and uses the following related information and function"
        if references is not None:
            if len(references) > 1:
                s += "s"
            s += ":"
            s += "\n\n"
            for key in references:
                s += f"- [{key}]({references[key]})\n"
        s += "\n\n##Note\n"
        s += "This node uses the Census Bureau Data API but is not endorsed or certified by the Census Bureau. "
        s += "For the terms of service click [here.](https://www.census.gov/data/developers/about/terms-of-service.html)"
        node_factory.__doc__ = s
        return node_factory

    return set_description

def osm_node_description(short_description: str, description: str, references: dict):
    """This decorator takes the provided information and generates a standardized node description
    for nodes that are based on OpenStreetMap data. It also places the proper copyright notice which
    is necessary."""

    def set_description(node_factory):
        s = f"{short_description}\n"
        s += f"{description}\n\n"
        # s += "___\n\n"  # separator line between description and general part
        s += "The node is based on the [OpenStreetMap project](https://www.openstreetmap.org/about) "
        s += "and uses the following related information and function"
        if references is not None:
            if len(references) > 1:
                s += "s"
            s += ":"
            s += "\n\n"
            for key in references:
                s += f"- [{key}]({references[key]})\n"
        s += "\n\n##Note\n"
        s += (
            "Data copyright by [OpenStreetMap](https://www.openstreetmap.org/copyright)"
        )
        s += "[(ODbl)](https://opendatacommons.org/licenses/odbl/index.html) and provided under "
        s += "[CC-BY-SA.](https://creativecommons.org/licenses/by-sa/2.0/)"
        s += "To report a problem and contribute to OpenStreetMap click [here.](https://www.openstreetmap.org/fixthemap)"
        s += "Please note the OpenStreetMap licence and attribution guidelines as described "
        s += "[here.](https://wiki.osmfoundation.org/wiki/Licence/Attribution_Guidelines)"
        node_factory.__doc__ = s
        return node_factory

    return set_description

def pd_node_description(short_description: str, description: str, references: dict):
    """This decorator takes the provided information and generates a standardized node description
    for nodes that are based on GeoPandas functionality."""

    def set_description(node_factory):
        s = f"{short_description}\n"
        s += f"{description}\n\n"
        # s += "___\n\n"  # separator line between description and general part
        s += "The node is based on the package [pandas](https://pandas.pydata.org/docs/reference/api/pandas.read_parquet.html)  and here are related tools and references"
        if references is not None:
            if len(references) > 1:
                s += "s"
            s += ":"
            s += "\n\n"
            for key in references:
                s += f"- [{key}]({references[key]})\n"
        node_factory.__doc__ = s
        return node_factory

    return set_description

def pulp_node_description(short_description: str, description: str, references: dict):
    """This decorator takes the provided information and generates a standardized node description
    for nodes that are based on GeoPandas functionality."""

    def set_description(node_factory):
        s = f"{short_description}\n"
        s += f"{description}\n\n"
        # s += "___\n\n"  # separator line between description and general part
        s += "The node is based on the package [PuLP](https://coin-or.github.io/pulp/)  and here are related tools and references"
        if references is not None:
            if len(references) > 1:
                s += "s"
            s += ":"
            s += "\n\n"
            for key in references:
                s += f"- [{key}]({references[key]})\n"
        node_factory.__doc__ = s
        return node_factory

    return set_description

def geoda_node_description(short_description: str, description: str, references: dict):
    """This decorator takes the provided information and generates a standardized node description
    for nodes that are based on pygeoda functionality."""

    def set_description(node_factory):
        s = f"{short_description}\n"
        s += f"{description}\n\n"
        # s += "___\n\n"  # separator line between description and general part
        s += "The node is based on the package [pygeoda](https://geodacenter.github.io/pygeoda/index.html) and here are related tools and references:\n\n"
        if references is not None:
            for key in references:
                s += f"- [{key}]({references[key]})\n"
        node_factory.__doc__ = s
        return node_factory

    return set_description

############################################
# GeoPandas helper
############################################
def load_geo_data_frame(
    input_table: knext.Table,
    column: knext.Column,
    exec_context: knext.ExecutionContext = None,
    load_msg: str = "Loading Geo data frame...",
    done_msg: str = "Geo data frame loaded. Start computation...",
) -> gp.GeoDataFrame:
    """Creates a GeoDataFrame from the given input table using the provided column as geo column."""
    if exec_context:
        exec_context.set_progress(0.0, load_msg)
    gdf = gp.GeoDataFrame(input_table.to_pandas(), geometry=column)
    if exec_context:
        exec_context.set_progress(0.1, done_msg)
    return gdf

def to_table(
    gdf: gp.GeoDataFrame,
    exec_context: knext.ExecutionContext = None,
    done_msg: str = "Computation done",
) -> knext.Table:
    """Returns a KNIME table representing the given GeoDataFrame."""
    if exec_context:
        exec_context.set_progress(1.0, done_msg)
    return knext.Table.from_pandas(gdf)

############################################
# General helper
############################################

__DEF_PLEASE_SELECT_COLUMN = "Please select a column"

def column_exists_or_preset(
    context: knext.ConfigurationContext,
    column: str,
    schema: knext.Schema,
    func: Callable[[knext.Column], bool] = None,
    none_msg: str = "No compatible column found in input table",
) -> str:
    """
    Checks that the given column is not None and exists in the given schema. If none is selected it returns the
    first column that is compatible with the provided function. If none is compatible it throws an exception.
    """
    if column is None:
        for c in schema:
            if func(c):
                context.set_warning(f"Preset column to: {c.name}")
                return c.name
        raise knext.InvalidParametersError(none_msg)
    __check_col_and_type(column, schema, func)
    return column

def geo_column_exists(
    column: str,
    schema: knext.Schema,
    none_msg: str = __DEF_PLEASE_SELECT_COLUMN,
) -> None:
    """
    Checks that the given column is not None, of type geo and exists in the given schema otherwise it throws an exception
    """
    return column_exists(column, schema, is_geo, none_msg)

def column_exists(
    column: str,
    schema: knext.Schema,
    func: Callable[[knext.Column], bool] = None,
    none_msg: str = __DEF_PLEASE_SELECT_COLUMN,
) -> None:
    """
    Checks that the given column is not None and exists in the given schema otherwise it throws an exception
    """
    if column is None:
        raise knext.InvalidParametersError(none_msg)
    __check_col_and_type(column, schema, func)

def __check_col_and_type(
    column: str,
    schema: knext.Schema,
    check_type: Callable[[knext.Column], bool] = None,
) -> None:
    """
    Checks that the given column exists in the given schema and that it matches the given type_check function.
    """
    # Check that the column exists in the schema and that it has a compatible type
    try:
        existing_column = schema[column]
        if check_type is not None and not check_type(existing_column):
            raise knext.InvalidParametersError(
                f"Column '{str(column)}' has incompatible data type"
            )
    except IndexError:
        raise knext.InvalidParametersError(
            f"Column '{str(column)}' not available in input table"
        )

def columns_exist(
    columns: List[str],
    schema: knext.Schema,
    func: Callable[[knext.Column], bool] = lambda c: True,
    none_msg: str = __DEF_PLEASE_SELECT_COLUMN,
) -> None:
    """
    Checks that the given columns are not None and exist in the given schema otherwise it throws an exception
    """
    for col in columns:
        column_exists(col, schema, func, none_msg)

def fail_if_column_exists(
    column_name: str, input_schema: knext.Schema, msg: str = None
):
    """Checks that the given column name does not exists in the input schema.
    Can be used to check that a column is not accidentally overwritten."""
    if column_name in input_schema.column_names:
        if msg is None:
            msg = f"Column '{column_name}' exists"
        raise knext.InvalidParametersError(msg)

def get_unique_column_name(column_name: str, input_schema: knext.Schema) -> str:
    """Checks if the column name exists in the given schema and if so appends a number to it to make it unique.
    The unique name if returned or the original if it was already unique."""
    return get_unique_name(column_name, input_schema.column_names)
    # if column_name is None:
    #     raise knext.InvalidParametersError("Column name must not be None")
    # uniquifier = 1
    # result = column_name
    # while result in input_schema.column_names:
    #     result = column_name + f"(#{uniquifier})"
    #     uniquifier += 1
    # return result

def get_unique_name(column_name: str, existing_col_names) -> str:
    """Checks if the column name exists in the given schema and if so appends a number to it to make it unique.
    The unique name if returned or the original if it was already unique."""
    if column_name is None:
        raise knext.InvalidParametersError("Column name must not be None")
    uniquifier = 1
    result = column_name
    while result in existing_col_names:
        result = column_name + f"(#{uniquifier})"
        uniquifier += 1
    return result

def check_canceled(exec_context: knext.ExecutionContext) -> None:
    """
    Checks if the user has canceled the execution and if so throws a RuntimeException
    """
    if exec_context.is_canceled():
        raise RuntimeError("Execution canceled")

def ensure_file_extension(file_name: str, file_extension: str) -> str:
    """
    Checks if the given file_name ends with the given file_extension and if not appends it to the returned file_name.
    """
    if not file_name:
        raise knext.InvalidParametersError("Please enter a valid file name")
    if file_name.lower().endswith(file_extension):
        return file_name
    return file_name + file_extension

class ResultSettingsMode(knext.EnumParameterOptions):
    REPLACE = (
        "Replace",
        "Replace the selected input column with the result.",
    )
    APPEND = (
        "Append",
        "Append a new column with the name provided below.",
    )

    @classmethod
    def get_default(cls):
        return cls.REPLACE

@knext.parameter_group(label="Output", since_version="1.1.0")
class ResultSettings:
    """
    Group of settings that define the format of the result table.
    """

    def __init__(self, mode=ResultSettingsMode.get_default().name, new_name="geometry"):
        mode_param = knext.EnumParameter(
            label="Output column",
            description="Choose where to place the result column:",
            default_value=mode,
            enum=ResultSettingsMode,
        )
        self.mode = mode_param
        self.new_column_name = knext.StringParameter(
            "New column name",
            "The name of the new column that is appended if 'Append' is selected.",
            default_value=new_name,
        ).rule(
            knext.OneOf(mode_param, [ResultSettingsMode.APPEND.name]), knext.Effect.SHOW
        )

    def get_result_schema(
        self,
        configure_context: knext.ConfigurationContext,
        schema: knext.Schema,
        selected_col: knext.Column,
        result_type,
    ) -> knext.Schema:
        """
        Either replaces the selected column or appends a new column to the end.
        """
        if self.mode == ResultSettingsMode.REPLACE.name:
            col_names = schema.column_names
            i = 0
            while i < len(col_names):
                if col_names[i] == selected_col:
                    result_schema = schema.remove(i)
                    return result_schema.insert(
                        knext.Column(result_type, selected_col), i
                    )
                i += 1
            raise knext.InvalidParametersError(
                f"Selected column '{selected_col}' not found"
            )
        # make sure the appended column is unique
        result_col = get_unique_column_name(self.new_column_name, schema)
        return schema.append(knext.Column(result_type, result_col))

    def get_result_table(
        self,
        exec_context: knext.ExecutionContext,
        gdf: gp.GeoDataFrame,
        selected_col: knext.Column,
        result_col: str,
    ) -> gp.GeoDataFrame:
        """
        Assumes that the result_col and the select_col are part of the input data frame.
        The (altered) input data frame is returned.
        """
        if self.mode == ResultSettingsMode.REPLACE.name:
            check_canceled(exec_context)
            exec_context.set_progress(0.9, "Replace input column with result column")
            gdf[selected_col] = gdf[result_col]
            gdf.drop(result_col, axis=1, inplace=True)
            gdf.rename(columns={self.new_column_name: selected_col}, inplace=True)
            return gdf
        return gdf

    def get_computed_result_table(
        self,
        exec_context: knext.ExecutionContext,
        input_table: knext.Table,
        selected_col: knext.Column,
        func: Callable,
    ) -> knext.Table:
        """
        Uses the given function to either append a new column or replace the existing column to the given input table and
        returns the result as a table depending on the user chosen settings.
        """
        gdf = load_geo_data_frame(input_table, selected_col, exec_context)
        gdf = self.get_computed_result_frame(
            exec_context, input_table.schema, gdf, selected_col, func
        )
        return to_table(gdf, exec_context)

    def get_computed_result_frame(
        self,
        exec_context: knext.ExecutionContext,
        schema: knext.Schema,
        gdf: gp.GeoDataFrame,
        selected_col: knext.Column,
        func: Callable,
    ) -> knext.Table:
        """
        Uses the given function to either append a new column or replace the existing column to the given input
        GeoDataFrame and returns the result as a table depending on the user chosen settings.
        """
        result_col = selected_col
        if self.mode == ResultSettingsMode.APPEND.name:
            result_col = get_unique_column_name(self.new_column_name, schema)
        gdf[result_col] = gdf.apply(lambda l: func(l[selected_col]), axis=1)
        return gdf

def get_env_path():
    """
    Returns the path to the used Python environment e.g. the Python packages of this environment can be found in
    <RETURNED_VAL>\Lib\site-packages.
    """
    import sys
    import os.path as os

    # path to the Python executable in the used Python environment
    exec_path = sys.executable
    env_path = os.dirname(exec_path)
    return env_path

# def re_order_weight_rows(gdf, adjust_list, id_col):
#     """
#     Reorder the spatial weight according to the id_col.
#     """
#     # Reorder the rows based on the weight column
#     import libpysal

#     gdf.index = range(len(gdf))
#     w_ref = libpysal.weights.Rook.from_dataframe(gdf)
#     id_map = gdf[id_col].to_dict()
#     w_ref.transform = "r"
#     adjust_list_ref = w_ref.to_adjlist()
#     for k, row in adjust_list_ref.iterrows():
#         focal = id_map[row["focal"]]
#         neighbor = id_map[row["neighbor"]]
#         row["weight"] = adjust_list[
#             (adjust_list["focal"] == focal) & (adjust_list["neighbor"] == neighbor)
#         ]["weight"]

#     return adjust_list_ref


from flask import Blueprint
from flask_restful import Api
from .views import UserInfoResource, UserInviteesMessage, UserMessageResource, UserMessageDetailResource,\
    UserOrganizationResource

blueprint = Blueprint('user', __name__)
api = Api(blueprint)

api.add_resource(UserInfoResource, '/info')

api.add_resource(UserInviteesMessage, '/org/request/<string:org_request_id>')
api.add_resource(UserMessageDetailResource, '/message/<string:message_id>')
api.add_resource(UserMessageResource, '/message')
api.add_resource(UserOrganizationResource, '/org/<string:org_id>')


from __future__ import annotations
from typing import Optional, NamedTuple

from ngsolve import *
from . import io
from .sensor import Sensor
from .formulations import formulation_factory, _Formulation
from .configuration import SolverConfiguration, DreAmLogger
from math import isnan

import logging
logger = logging.getLogger('DreAm.Solver')

class IterationError(NamedTuple):
    error: float
    iteration_number: Optional[int]
    time: Optional[float]

    def __repr__(self) -> str:

        error_digit = DreAmLogger._iteration_error_digit
        time_step_digit = DreAmLogger._time_step_digit

        string = f"Iteration Error: {self.error:{error_digit}e}"
        if self.iteration_number is not None:
            string += f" - Iteration Number: {self.iteration_number}"
        if self.time is not None:
            string += f" - Time Step: {self.time:.{time_step_digit}f}"

        return string

class SolverStatus:

    def __init__(self, solver_configuration: SolverConfiguration) -> None:
        self.solver_configuration = solver_configuration
        self.reset()

    @property
    def is_converged(self) -> bool:
        return self._is_converged

    @property
    def is_nan(self) -> bool:
        return self._is_nan

    @property
    def iteration_error(self) -> IterationError:
        return self._iteration_error

    @iteration_error.setter
    def iteration_error(self, value: IterationError):
        min_convergence = self.solver_configuration.convergence_criterion

        self._iteration_error = value
        self._is_converged = value.error < min_convergence

        if isnan(value.error):
            self._is_nan = True
            logger.error("Solution process diverged!")

    def reset_convergence_status(self):
        self._is_converged = False
        self._is_nan = False

    def reset(self):
        self.reset_convergence_status()
        self.iteration_error_list = []

    def check_convergence(self,
                          step_direction,
                          residual,
                          time: Optional[float] = None,
                          iteration_number: Optional[int] = None) -> None:

        error = sqrt(InnerProduct(step_direction, residual)**2)
        self.iteration_error = IterationError(error, iteration_number, time)

        if logger.hasHandlers():
            logger.info(self.iteration_error)

class CompressibleHDGSolver:

    def __init__(self, mesh: Mesh,
                 solver_configuration: SolverConfiguration,
                 directory_tree: Optional[io.ResultsDirectoryTree] = None):

        if directory_tree is None:
            directory_tree = io.ResultsDirectoryTree()

        self._formulation = formulation_factory(mesh, solver_configuration)
        self._status = SolverStatus(self.solver_configuration)
        self._sensors = []
        self._drawer = io.Drawer(self.formulation)
        self._directory_tree = directory_tree

    @property
    def formulation(self) -> _Formulation:
        return self._formulation

    @property
    def dmesh(self):
        return self.formulation.dmesh

    @property
    def mesh(self):
        return self.formulation.mesh

    @property
    def solver_configuration(self):
        return self.formulation.cfg

    @property
    def boundary_conditions(self):
        return self.formulation.dmesh.bcs

    @property
    def domain_conditions(self):
        return self.formulation.dmesh.dcs

    @property
    def sensors(self) -> list[Sensor]:
        return self._sensors

    @property
    def status(self) -> SolverStatus:
        return self._status

    @property
    def drawer(self) -> io.Drawer:
        return self._drawer

    @property
    def directory_tree(self) -> io.ResultsDirectoryTree:
        return self._directory_tree

    def setup(self, reinitialize: bool = True):

        if self.formulation.dmesh.is_grid_deformation:
            self.formulation.dmesh.set_grid_deformation()

        if reinitialize:
            self.formulation.initialize()

        self.residual = self.formulation.gfu.vec.CreateVector()
        self.temporary = self.formulation.gfu.vec.CreateVector()

        self._set_linearform()
        self._set_bilinearform()

        if reinitialize and self.domain_conditions.initial_conditions:
            self._solve_initial()

    def solve_stationary(self,
                         increment_at_iteration: int = 10,
                         increment_time_step_factor: int = 10,
                         state_name: str = "stationary",
                         stop_at_iteration: bool = False) -> bool:

        self.solver_configuration.time.simulation = 'stationary'
        max_iterations = self.solver_configuration.max_iterations

        self.status.reset()
        saver = self.get_saver()

        for it in range(max_iterations):

            self.formulation.update_gridfunctions()
            self._update_pseudo_time_step(it, increment_at_iteration, increment_time_step_factor)

            self._solve_update_step()
            self.status.check_convergence(self.temporary, self.residual, iteration_number=it)

            self.drawer.redraw()

            if stop_at_iteration:
                input("Iteration stopped. Hit any key to continue.")

            if self.status.is_nan:
                break

            self._update_solution()

            if self.status.is_converged:
                break

        for sensor in self.sensors:
            sensor.take_single_sample()

        if self.solver_configuration.save_state:
            saver.save_state(name=state_name)

        self.drawer.redraw()

        self.formulation.update_gridfunctions()

    def solve_transient(self, state_name: str = "transient",  save_state_every_num_step: int = 1):

        self.solver_configuration.time.simulation = 'transient'

        self.status.reset()
        saver = self.get_saver()

        for idx, t in enumerate(self.solver_configuration.time):
            self._solve_timestep(t)

            for sensor in self.sensors:
                sensor.take_single_sample(t)

            if self.solver_configuration.save_state:
                if idx % save_state_every_num_step == 0:
                    saver.save_state(name=f"{state_name}_{t}")

            if self.status.is_nan:
                break

    def solve_perturbation(self) -> GridFunction:

        lf = LinearForm(self.formulation.fes)
        self.formulation.add_perturbation_linearform(lf)
        perturbation_gfu = self._solve_mass(lf)
        for gfu in self.formulation.gridfunctions.values():
            gfu.vec.data += perturbation_gfu.vec

        self.drawer.redraw()
        logger.info("Perturbation applied!")
        return perturbation_gfu

    def add_sensor(self, sensor: Sensor):
        sensor.assign_solver(self)
        self.sensors.append(sensor)

    def get_saver(self, directory_tree: Optional[io.ResultsDirectoryTree] = None) -> io.SolverSaver:
        saver = io.SolverSaver(self)
        if directory_tree is not None:
            saver.tree = directory_tree
        return saver

    def get_loader(self, directory_tree: Optional[io.ResultsDirectoryTree] = None) -> io.SolverLoader:
        loader = io.SolverLoader(self)
        if directory_tree is not None:
            loader.tree = directory_tree
        return loader

    def _set_linearform(self):

        fes = self.formulation.fes
        self.f = LinearForm(fes)
        self.formulation.add_forcing_linearform(self.f)
        self.f.Assemble()

    def _set_bilinearform(self):

        fes = self.formulation.fes

        condense = self.solver_configuration.static_condensation
        mu = self.solver_configuration.dynamic_viscosity

        self.blf = BilinearForm(fes, condense=condense)

        self.formulation.add_time_bilinearform(self.blf)
        self.formulation.add_convective_bilinearform(self.blf)

        if not mu.is_inviscid:
            self.formulation.add_diffusive_bilinearform(self.blf)

        self.formulation.add_boundary_conditions_bilinearform(self.blf)
        self.formulation.add_domain_conditions_bilinearform(self.blf)

    def _solve_mass(self, linearform: LinearForm) -> GridFunction:
        formulation = self.formulation
        gfu = GridFunction(formulation.fes)

        blf = BilinearForm(formulation.fes)
        formulation.add_mass_bilinearform(blf)

        blf.Assemble()
        linearform.Assemble()

        blf_inverse = blf.mat.Inverse(formulation.fes.FreeDofs(), inverse="sparsecholesky")

        gfu.vec.data = blf_inverse * linearform.vec
        return gfu

    def _solve_initial(self):
        lf = LinearForm(self.formulation.fes)
        self.formulation.add_initial_linearform(lf)
        self.formulation.gfu.vec.data = self._solve_mass(lf).vec
        self.formulation.update_gridfunctions(initial_value=True)

    def _solve_update_step(self):

        linear_solver = self.solver_configuration.linear_solver
        fes = self.formulation.fes
        gfu = self.formulation.gfu

        self.blf.Apply(gfu.vec, self.residual)
        self.residual.data -= self.f.vec
        self.blf.AssembleLinearization(gfu.vec)

        inv = self.blf.mat.Inverse(fes.FreeDofs(self.blf.condense), inverse=linear_solver)
        if self.blf.condense:
            self.residual.data += self.blf.harmonic_extension_trans * self.residual
            self.temporary.data = inv * self.residual
            self.temporary.data += self.blf.harmonic_extension * self.temporary
            self.temporary.data += self.blf.inner_solve * self.residual
        else:
            self.temporary.data = inv * self.residual

    def _update_solution(self):
        damping_factor = self.solver_configuration.damping_factor
        gfu = self.formulation.gfu

        gfu.vec.data -= damping_factor * self.temporary

    def _update_pseudo_time_step(self,
                                 iteration_number: int,
                                 increment_at_iteration: int = 10,
                                 increment_time_step_factor: int = 10):

        cfg = self.solver_configuration.time

        max_time_step = cfg.max_step
        old_time_step = cfg.step.Get()

        if max_time_step > old_time_step:
            if (iteration_number % increment_at_iteration == 0) and (iteration_number > 0):
                new_time_step = old_time_step * increment_time_step_factor

                if new_time_step > max_time_step:
                    new_time_step = max_time_step

                cfg.step = new_time_step
                logger.info(f"Successfully updated time step at iteration {iteration_number}")
                logger.info(f"Updated time step 𝚫t = {new_time_step}. Previous time step 𝚫t = {old_time_step}")

    def _solve_timestep(self, time, stop_at_iteration: bool = False) -> bool:

        max_iterations = self.solver_configuration.max_iterations

        self.status.reset_convergence_status()

        for it in range(max_iterations):

            self._solve_update_step()
            self.status.check_convergence(self.temporary, self.residual, time, it)

            if self.status.is_nan:
                break

            self._update_solution()

            if stop_at_iteration:
                input("Iteration stopped. Hit any key to continue.")

            if self.status.is_converged:
                break

        self.formulation.update_gridfunctions()

        self.drawer.redraw()


# -*- coding: utf-8 -*-
import scrapy

class DemoScrapeSpiderXPath(scrapy.Spider):
    name = 'demo-xpath'
    start_urls = [
        'https://www.dexciss.com/?page_id=213',
    ]

    def parse(self, response):
        for quote in response.xpath('//div[@class="et_pb_text_inner"]'):
            yield {
                # 'brochure': quote.xpath('.//h1/text()').extract(),
                # 'details': quote.xpath('.//p/text()').extract(),
                'brochures': quote.xpath('.//h1/text()').extract(),
                'details': quote.xpath('.//div[@class="et_pb_text_inner"]/p/text()').extract(),
            }

        # next_page_url = response.xpath('//li[@class="next"]/a/@href').extract_first()
        # if next_page_url is not None:
        #     yield scrapy.Request(response.urljoin(next_page_url))


"""empty message

Revision ID: 8a733a25dd3c
Revises: 8e70f2f40ee6
Create Date: 2019-06-19 04:22:13.097149

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '8a733a25dd3c'
down_revision = '8e70f2f40ee6'
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_unique_constraint('unique_email', 'user_v2', ['email'])
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_constraint('unique_email', 'user_v2', type_='unique')
    # ### end Alembic commands ###


import os
import sys
from setuptools import setup, find_packages
from tethys_apps.app_installation import custom_develop_command, custom_install_command

# -- Apps Definition -- #
app_package = 'hydroshare_data_viewer'
release_package = 'tethysapp-' + app_package
app_class = 'hydroshare_data_viewer.app:HydroshareDataViewer'
app_package_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tethysapp', app_package)

# -- Python Dependencies -- #
dependencies = []

setup(
    name=release_package,
    version='1.0',
    tags='',
    description='This app is designed to run on Tethys Platform and helps support CUAHSI\'s HydroShare project. Its purpose is to allow HydroShare users to quickly preview hydrologic geospatial and time series content stored in HydroShare resources.',
    long_description='This app is designed to run on Tethys Platform and helps support CUAHSI\'s HydroShare project. Its purpose is to allow HydroShare users to quickly preview hydrologic geospatial and time series content stored in HydroShare resources.',
    keywords='',
    author='Ken Lippold',
    author_email='kjlippold@gmail.com',
    url='https://hs-apps-dev.hydroshare.org/apps/hydroshare-data-viewer/',
    license='MIT',
    packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
    namespace_packages=['tethysapp', 'tethysapp.' + app_package],
    include_package_data=True,
    zip_safe=False,
    install_requires=dependencies,
    cmdclass={
        'install': custom_install_command(app_package, app_package_dir, dependencies),
        'develop': custom_develop_command(app_package, app_package_dir, dependencies)
    }
)


import os
import cairosvg
from PIL import Image
import xml.etree.ElementTree as ET

def add_or_modify_stroke_width(element, stroke_width):
    """Add or modify stroke-width in the style attribute."""
    style = element.get('style', '')
    if 'stroke-width' not in style:
        new_style = f'stroke-width:{stroke_width};{style}' if style else f'stroke-width:{stroke_width}'
        element.set('style', new_style)

def set_attributes_recursively(element, stroke_width):
    """Set stroke-width attribute recursively on all subelements."""
    for subelement in element.iter():
        add_or_modify_stroke_width(subelement, stroke_width)

def modify_svg(svg_file, stroke_width=".8px"):
    tree = ET.parse(svg_file)
    root = tree.getroot()

    # Modify stroke-width
    set_attributes_recursively(root, stroke_width)

    tree.write(svg_file)  # Overwrite the original SVG or write to a new file

def svg_to_png(svg_file, output_png, png_width=180):
    """Convert an SVG file to a PNG file with a specified width."""
    cairosvg.svg2png(url=svg_file, write_to=output_png, output_width=png_width)

def combine_center_align(png_file1, png_file2, output_file):
    """Combine two PNG files and center align them."""
    # Open the images
    img1 = Image.open(png_file1)
    img2 = Image.open(png_file2)

    # Calculate dimensions for the output image
    output_width = max(img1.width, img2.width)
    output_height = max(img1.height, img2.height)

    # Create a new image with the calculated dimensions
    combined_img = Image.new('RGBA', (output_width, output_height), (255, 255, 255, 0))

    # Calculate positions for center alignment
    img1_x = (output_width - img1.width) // 2
    img1_y = (output_height - img1.height) // 2
    img2_x = (output_width - img2.width) // 2
    img2_y = (output_height - img2.height) // 2

    # Paste the images onto the combined image
    combined_img.paste(img2, (img2_x, img2_y), img2 if img2.mode == 'RGBA' else None)
    combined_img.paste(img1, (img1_x, img1_y), img1 if img1.mode == 'RGBA' else None)

    # Save the combined image
    combined_img.save(output_file)

# Define file paths
svg_directory = 'source/'
export_directory = 'export/'
existing_png = 'back.png'

# Ensure export directory exists
if not os.path.exists(export_directory):
    os.makedirs(export_directory)

# Process each SVG in the directory
for filename in os.listdir(svg_directory):
    if filename.endswith(".svg"):
        svg_file = os.path.join(svg_directory, filename)
        base_name = os.path.splitext(filename)[0]
        exported_png = os.path.join(export_directory, f"{base_name}.png")
        output_png = os.path.join(export_directory, f"{base_name}.png")

        # Modify stroke-width and convert to PNG
        modify_svg(svg_file, "1.5px")  # Adjust stroke-width as needed
        svg_to_png(svg_file, exported_png)

        # Combine the exported PNG with an existing PNG
        combine_center_align(exported_png, existing_png, output_png)

        print(f"Processed and combined {filename}")


import os
import torch
import numpy as np
from PIL import Image
from collections import defaultdict
from torch.utils.data import Dataset
from torchvision import transforms as T

class MVSDataset(Dataset):
    def __init__(self, root_dir, split='intermediate', n_views=3, levels=3, img_wh=(1152, 864)):
        assert img_wh[0] % 32==0 and img_wh[1] % 32==0, \
            'img_wh must both be multiples of 32!'
        assert split in ['intermediate', 'advanced'], \
            'split must be either "intermediate", "advanced"!'
        self.img_wh = img_wh
        self.root_dir = root_dir

        self.split = split
        self.n_views = n_views
        self.levels = levels # FPN levels
        self.build_metas()
        self.define_transforms()

    def build_metas(self):
        self.metas = []
        if self.split == 'intermediate':
            self.scans = ['Family', 'Francis', 'Horse', 'Lighthouse',
                          'M60', 'Panther', 'Playground', 'Train']
            self.image_sizes = {'Family': (1920, 1080),
                                'Francis': (1920, 1080),
                                'Horse': (1920, 1080),
                                'Lighthouse': (2048, 1080),
                                'M60': (2048, 1080),
                                'Panther': (2048, 1080),
                                'Playground': (1920, 1080),
                                'Train': (1920, 1080)}
            self.depth_interval = {'Family': 2.5e-3,
                                   'Francis': 1e-2,
                                   'Horse': 1.5e-3,
                                   'Lighthouse': 1.5e-2,
                                   'M60': 5e-3,
                                   'Panther': 5e-3,
                                   'Playground': 7e-3,
                                   'Train': 5e-3} # depth interval for each scan (hand tuned)
        elif self.split == 'advanced':
            self.scans = ['Auditorium', 'Ballroom', 'Courtroom',
                          'Museum', 'Palace', 'Temple']
            self.image_sizes = {'Auditorium': (1920, 1080),
                                'Ballroom': (1920, 1080),
                                'Courtroom': (1920, 1080),
                                'Museum': (1920, 1080),
                                'Palace': (1920, 1080),
                                'Temple': (1920, 1080)}
            self.depth_interval = {'Auditorium': 3e-2,
                                   'Ballroom': 2e-2,
                                   'Courtroom': 2e-2,
                                   'Museum': 2e-2,
                                   'Palace': 1e-2,
                                   'Temple': 1e-2} # depth interval for each scan (hand tuned)
        self.ref_views_per_scan = defaultdict(list)

        for scan in self.scans:
            with open(os.path.join(self.root_dir, self.split, scan, 'pair.txt')) as f:
                num_viewpoint = int(f.readline())
                for _ in range(num_viewpoint):
                    ref_view = int(f.readline().rstrip())
                    src_views = [int(x) for x in f.readline().rstrip().split()[1::2]]
                    if len(src_views) <= 0: continue
                    self.metas += [(scan, -1, ref_view, src_views)]
                    self.ref_views_per_scan[scan] += [ref_view]

    def read_cam_file(self, filename):
        with open(filename) as f:
            lines = [line.rstrip() for line in f.readlines()]
        # extrinsics: line [1,5), 4x4 matrix
        extrinsics = np.fromstring(' '.join(lines[1:5]), dtype=np.float32, sep=' ')
        extrinsics = extrinsics.reshape((4, 4))
        # intrinsics: line [7-10), 3x3 matrix
        intrinsics = np.fromstring(' '.join(lines[7:10]), dtype=np.float32, sep=' ')
        intrinsics = intrinsics.reshape((3, 3))
        # depth_min & depth_interval: line 11
        depth_min = float(lines[11].split()[0])
        return intrinsics, extrinsics, depth_min

    def define_transforms(self):
        self.transform = T.Compose([T.ToTensor(),
                                    T.Normalize(mean=[0.485, 0.456, 0.406],
                                                std=[0.229, 0.224, 0.225]),
                                    ])

    def __len__(self):
        return len(self.metas)

    def __getitem__(self, idx):
        sample = {}
        scan, _, ref_view, src_views = self.metas[idx]
        # use only the reference view and first nviews-1 source views
        view_ids = [ref_view] + src_views[:self.n_views-1]

        imgs = []
        proj_mats = [] # record proj mats between views
        for i, vid in enumerate(view_ids):
            img_filename = os.path.join(
                self.root_dir, self.split, scan, f'images/{vid:08d}.jpg'
            )
            proj_mat_filename = os.path.join(
                self.root_dir, self.split, scan, f'cams_shortrange/{vid:08d}_cam.txt'
            )
            img = Image.open(img_filename)
            img = img.resize(self.img_wh, Image.BILINEAR)
            img = self.transform(img)
            imgs += [img]

            img_w, img_h = self.image_sizes[scan]
            intrinsics, extrinsics, depth_min = self.read_cam_file(proj_mat_filename)
            intrinsics[0] *= self.img_wh[0] / img_w / 4
            intrinsics[1] *= self.img_wh[1] / img_h / 4
            proj_mat = np.zeros(shape=(2, 4, 4), dtype=np.float32)
            proj_mat[0, :4, :4] = extrinsics
            proj_mat[1, :3, :3] = intrinsics
            proj_mats.append(torch.FloatTensor(proj_mat))

            if i == 0:  # reference view
                depth_max = self.depth_interval[scan] * 192 + depth_min
                depth_values = torch.arange(depth_min, depth_max, self.depth_interval[scan])

        imgs = torch.stack(imgs) # (V, 3, H, W)

        # ms proj_mats
        proj_mats_stage1 = torch.stack(proj_mats)
        proj_mats_stage2 = proj_mats_stage1.clone()
        proj_mats_stage2[:, 1, :2, :] = proj_mats_stage2[:, 1, :2, :] * 2
        proj_mats_stage3 = proj_mats_stage1.clone()
        proj_mats_stage3[:, 1, :2, :] = proj_mats_stage3[:, 1, :2, :] * 4
        proj_mats_ms = {
            "stage1": proj_mats_stage1,
            "stage2": proj_mats_stage2,
            "stage3": proj_mats_stage3
        }

        sample['imgs'] = imgs
        sample['proj_mats'] = proj_mats_ms
        sample['depth_values'] = depth_values
        sample['filename'] = scan + '/{}/' + '{:0>8}'.format(view_ids[0]) + "{}"

        return sample


#!/usr/bin/env python
# -*- coding: utf-8 -*-

""" ~LCD Game Shrinker~
custom rules to apply to the same basename game.
The keys mapping need to be defined. The information can
found in 'hh_sm510.cpp' MAME driver.
More advanced image transformation can be created using this file.

This program permits to shrink MAME high resolution artwork and
 graphics for protable device running LCD game emulator.

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <http://www.gnu.org/licenses/>.
"""
__author__ = "bzhxx"
__contact__ = "https://github.com/bzhxx"
__license__ = "GPLv3"

import os
from PIL import Image

import rom_config as rom

# Patch address to synchronize TIME with RTC host
rom.ADD_TIME_HOUR_MSB=64
rom.ADD_TIME_HOUR_LSB=65
rom.ADD_TIME_MIN_MSB=66
rom.ADD_TIME_MIN_LSB=67
rom.ADD_TIME_SEC_MSB=68
rom.ADD_TIME_SEC_LSB=69
rom.ADD_TIME_HOUR_MSB_PM_VALUE = 0

# gradient style
#Mix: background_file + 'bubbles.png' + 'background.png'
main_background_file = 'Background.png'
bubbles_file = 'Bubbles.png'

# experimental drop shadow effect on LCD segments
#rom.drop_shadow = True

score_board = Image.open(os.path.join(rom.mame_rom_dir, rom.background_file))
main_background = Image.open(os.path.join(
    rom.mame_rom_dir, main_background_file)).resize((score_board.size))
bubbles = Image.open(os.path.join(
    rom.mame_rom_dir, bubbles_file)).resize((score_board.size))

# create an empty image
img_background = Image.new("RGBA", score_board.size)

# add main background grey
img_background = Image.alpha_composite(img_background, main_background)

# vermin lines and score board
img_background = Image.alpha_composite(img_background, score_board)

# add bubbles
#img_background = Image.alpha_composite(img_background, bubbles)

# remove ALPHA channel
img_background = img_background.convert('RGB')

# save it
rom.background_file = "composite.png"
img_background_file = os.path.join(rom.mame_rom_dir, rom.background_file)
img_background.save(img_background_file)

# During vermin extermination, the animation is not visible with flag_lcd_deflicker_level = 2
rom.flag_lcd_deflicker_level = 1

# Input R(2)
K1 = rom.BTN_SHORTCUT_B_TIME
K2 = rom.BTN_TIME
K3 = rom.BTN_GAME
K4 = 0

rom.BTN_DATA[rom.R2] = K1 | (K2 << 8) | (K3 << 16) | (K4 << 24)

rom.BTN_DATA[rom.BA] = rom.BTN_RIGHT + rom.BTN_A
rom.BTN_DATA[rom.B] = rom.BTN_LEFT


import urllib.request
import time
import json
import datetime
import market
from market import CryptoMarket

cm = CryptoMarket()
myMoney = cm.buying_power
print(myMoney)
lookingToBuy = True;
ethBought = 0
boughtValue = 0
sellValue = 0
interestGain = 1.003
interestGainAfterSell = .008
sellValueIncreasePercent = 1.00002
noSellYet = True

def apiCall(callType):
    if(callType == 'history'):
        return json.load(urllib.request.urlopen('https://min-api.cryptocompare.com/data/histominute?fsym=ETH&tsym=USD&limit=1439'))['Data']
    else:
        if(lookingToBuy == True):
            return cm.get_buy_price()
        else:
            return cm.get_sell_price()

while True:
    now = datetime.datetime.now()

    try:
        currentValue = apiCall('price')
        pastValues = apiCall('history')
    except:
        print("API call failed, restarting loop")
        continue

    total = 0
    for value in pastValues:
        total += value['low']

    ceiling = total/len(pastValues)

    #print('Ceiling: ' + str(ceiling) + ' Current Value: ' + str(currentValue) +' time - '+ str(now.hour) +':' + str(now.minute)+':'+str(now.second))

    if currentValue < ceiling and lookingToBuy == True and (currentValue < sellValue - (sellValue * interestGainAfterSell) or noSellYet == True):
        print('Time to buy')
        newCurrentValue = 0
        while True:
            try:
                currentValue = apiCall('price')
                time.sleep(5)
                newCurrentValue = apiCall('price')
            except:
                print("API call failed in buy loop, restarting loop")
                continue

            if currentValue < newCurrentValue:
                break

        ethBought = cm.buying_power * 0.98 / currentValue
        cm.buy(amount=ethBought, price=newCurrentValue)
        print('Buying ' +str(ethBought) +' ethereum at ' + str(newCurrentValue)+' time - '+ str(now.hour) +':' + str(now.minute)+':'+str(now.second))
        lookingToBuy = False
        boughtValue = newCurrentValue

    if boughtValue > 0 and currentValue >= (boughtValue * interestGain):
        while True:
            try:
                currentValue = apiCall('price')
                time.sleep(5)
                newCurrentValue = apiCall('price')
            except:
                print("API call failed in sell loop, restarting loop")
                continue

            print('Looping until ' + str(newCurrentValue) + ' is less than ' + str(currentValue))
            if newCurrentValue  < currentValue:
                break

        cm.sell(amount=ethBought, price=currentValue)
        print('Selling ' + str(ethBought) + ' ethereum worth ' + str(boughtValue) + ' for ' + str(currentValue) +' time - '+ str(now.hour) +':' + str(now.minute)+':'+str(now.second))
        boughtValue = 0
        sellValue = currentValue
        lookingToBuy = True
        noSellYet = False

    sellValue = sellValue * sellValueIncreasePercent
    time.sleep(5)


import os
import re
import sys

import ctypes
import ctypes.util

import pyglet

_debug_lib = pyglet.options['debug_lib']
_debug_trace = pyglet.options['debug_trace']

_is_pyglet_doc_run = getattr(sys, "is_pyglet_doc_run", False)

if pyglet.options['search_local_libs']:
    script_path = pyglet.resource.get_script_home()
    cwd = os.getcwd()
    _local_lib_paths = [script_path, os.path.join(script_path, 'lib'), os.path.join(cwd, 'lib')]
    if pyglet.compat_platform == 'win32':
        os.environ["PATH"] += os.pathsep + os.pathsep.join(_local_lib_paths)
else:
    _local_lib_paths = None

class _TraceFunction:
    def __init__(self, func):
        self.__dict__['_func'] = func

    def __str__(self):
        return self._func.__name__

    def __call__(self, *args, **kwargs):
        return self._func(*args, **kwargs)

    def __getattr__(self, name):
        return getattr(self._func, name)

    def __setattr__(self, name, value):
        setattr(self._func, name, value)

class _TraceLibrary:
    def __init__(self, library):
        self._library = library
        print(library)

    def __getattr__(self, name):
        func = getattr(self._library, name)
        f = _TraceFunction(func)
        return f

if _is_pyglet_doc_run:
    class LibraryMock:
        """Mock library used when generating documentation."""
        def __getattr__(self, name):
            return LibraryMock()

        def __setattr__(self, name, value):
            pass

        def __call__(self, *args, **kwargs):
            return LibraryMock()

class LibraryLoader:

    platform = pyglet.compat_platform
    # this is only for library loading, don't include it in pyglet.platform
    if platform == 'cygwin':
        platform = 'win32'

    def load_library(self, *names, **kwargs):
        """Find and load a library.

        More than one name can be specified, they will be tried in order.
        Platform-specific library names (given as kwargs) are tried first.

        Raises ImportError if library is not found.
        """
        if _is_pyglet_doc_run:
            return LibraryMock()

        if 'framework' in kwargs and self.platform == 'darwin':
            return self.load_framework(kwargs['framework'])

        if not names:
            raise ImportError("No library name specified")

        platform_names = kwargs.get(self.platform, [])
        if isinstance(platform_names, str):
            platform_names = [platform_names]
        elif type(platform_names) is tuple:
            platform_names = list(platform_names)

        if self.platform.startswith('linux'):
            for name in names:
                libname = self.find_library(name)
                platform_names.append(libname or 'lib%s.so' % name)

        platform_names.extend(names)
        for name in platform_names:
            try:
                lib = ctypes.cdll.LoadLibrary(name)
                if _debug_lib:
                    print(name)
                if _debug_trace:
                    lib = _TraceLibrary(lib)
                return lib
            except OSError as o:
                path = self.find_library(name)
                if path:
                    try:
                        lib = ctypes.cdll.LoadLibrary(path)
                        if _debug_lib:
                            print(path)
                        if _debug_trace:
                            lib = _TraceLibrary(lib)
                        return lib
                    except OSError:
                        pass
                elif self.platform == "win32" and o.winerror != 126:
                    raise ImportError("Unexpected error loading library %s: %s" % (name, str(o)))

        raise ImportError('Library "%s" not found.' % names[0])

    def find_library(self, name):
        return ctypes.util.find_library(name)

    @staticmethod
    def load_framework(name):
        raise RuntimeError("Can't load framework on this platform.")

class MachOLibraryLoader(LibraryLoader):
    def __init__(self):
        if 'LD_LIBRARY_PATH' in os.environ:
            self.ld_library_path = os.environ['LD_LIBRARY_PATH'].split(':')
        else:
            self.ld_library_path = []

        if _local_lib_paths:
            # search first for local libs
            self.ld_library_path = _local_lib_paths + self.ld_library_path
            os.environ['LD_LIBRARY_PATH'] = ':'.join(self.ld_library_path)

        if 'DYLD_LIBRARY_PATH' in os.environ:
            self.dyld_library_path = os.environ['DYLD_LIBRARY_PATH'].split(':')
        else:
            self.dyld_library_path = []

        if 'DYLD_FALLBACK_LIBRARY_PATH' in os.environ:
            self.dyld_fallback_library_path = os.environ['DYLD_FALLBACK_LIBRARY_PATH'].split(':')
        else:
            self.dyld_fallback_library_path = [os.path.expanduser('~/lib'), '/usr/local/lib', '/usr/lib']

    def find_library(self, path):
        """Implements the dylib search as specified in Apple documentation:

        http://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/DynamicLibraryUsageGuidelines.html

        Before commencing the standard search, the method first checks
        the bundle's ``Frameworks`` directory if the application is running
        within a bundle (OS X .app).
        """

        libname = os.path.basename(path)
        search_path = []

        if '.dylib' not in libname:
            libname = 'lib' + libname + '.dylib'

        # py2app support
        if getattr(sys, 'frozen', None) == 'macosx_app' and 'RESOURCEPATH' in os.environ:
            search_path.append(os.path.join(os.environ['RESOURCEPATH'],
                                            '..',
                                            'Frameworks',
                                            libname))

        # conda support
        if os.environ.get('CONDA_PREFIX', False):
            search_path.append(os.path.join(os.environ['CONDA_PREFIX'], 'lib', libname))

        # pyinstaller.py sets sys.frozen to True, and puts dylibs in
        # Contents/MacOS, which path pyinstaller puts in sys._MEIPASS
        if (hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS') and
                sys.frozen is True and pyglet.compat_platform == 'darwin'):
            search_path.append(os.path.join(sys._MEIPASS, libname))

        if '/' in path:
            search_path.extend([os.path.join(p, libname) for p in self.dyld_library_path])
            search_path.append(path)
            search_path.extend([os.path.join(p, libname) for p in self.dyld_fallback_library_path])
        else:
            search_path.extend([os.path.join(p, libname) for p in self.ld_library_path])
            search_path.extend([os.path.join(p, libname) for p in self.dyld_library_path])
            search_path.append(path)
            search_path.extend([os.path.join(p, libname) for p in self.dyld_fallback_library_path])

        for path in search_path:
            if os.path.exists(path):
                return path

        return None

    @staticmethod
    def load_framework(name):
        path = ctypes.util.find_library(name)

        # Hack for compatibility with macOS > 11.0
        if path is None:
            frameworks = {
                'AGL': '/System/Library/Frameworks/AGL.framework/AGL',
                'IOKit': '/System/Library/Frameworks/IOKit.framework/IOKit',
                'OpenAL': '/System/Library/Frameworks/OpenAL.framework/OpenAL',
                'OpenGL': '/System/Library/Frameworks/OpenGL.framework/OpenGL'
            }
            path = frameworks.get(name)

        if path:
            lib = ctypes.cdll.LoadLibrary(path)
            if _debug_lib:
                print(path)
            if _debug_trace:
                lib = _TraceLibrary(lib)
            return lib

        raise ImportError("Can't find framework %s." % name)

class LinuxLibraryLoader(LibraryLoader):
    _ld_so_cache = None
    _local_libs_cache = None

    @staticmethod
    def _find_libs(directories):
        cache = {}
        lib_re = re.compile(r'lib(.*)\.so(?:$|\.)')
        for directory in directories:
            try:
                for file in os.listdir(directory):
                    match = lib_re.match(file)
                    if match:
                        # Index by filename
                        path = os.path.join(directory, file)
                        if file not in cache:
                            cache[file] = path
                        # Index by library name
                        library = match.group(1)
                        if library not in cache:
                            cache[library] = path
            except OSError:
                pass
        return cache

    def _create_ld_so_cache(self):
        # Recreate search path followed by ld.so.  This is going to be
        # slow to build, and incorrect (ld.so uses ld.so.cache, which may
        # not be up-to-date).  Used only as fallback for distros without
        # /sbin/ldconfig.
        #
        # We assume the DT_RPATH and DT_RUNPATH binary sections are omitted.

        directories = []
        try:
            directories.extend(os.environ['LD_LIBRARY_PATH'].split(':'))
        except KeyError:
            pass

        try:
            with open('/etc/ld.so.conf') as fid:
                directories.extend([dir.strip() for dir in fid])
        except IOError:
            pass

        directories.extend(['/lib', '/usr/lib'])

        self._ld_so_cache = self._find_libs(directories)

    def find_library(self, path):

        # search first for local libs
        if _local_lib_paths:
            if not self._local_libs_cache:
                self._local_libs_cache = self._find_libs(_local_lib_paths)
            if path in self._local_libs_cache:
                return self._local_libs_cache[path]

        # ctypes tries ldconfig, gcc and objdump.  If none of these are
        # present, we implement the ld-linux.so search path as described in
        # the man page.

        result = ctypes.util.find_library(path)

        if result:
            return result

        if self._ld_so_cache is None:
            self._create_ld_so_cache()

        return self._ld_so_cache.get(path)

if pyglet.compat_platform == 'darwin':
    loader = MachOLibraryLoader()
elif pyglet.compat_platform.startswith('linux'):
    loader = LinuxLibraryLoader()
else:
    loader = LibraryLoader()
load_library = loader.load_library


"""
Module for working with time envelopes.

Author: Boris Bantysh
E-mail: bbantysh60000@gmail.com
License: GPL-3.0
"""
from warnings import warn
from abc import ABC, abstractmethod
from typing import Optional, Callable, List
from fractions import Fraction

import numpy as np
from scipy.stats import norm

class Envelope(ABC):
    """Abstract envelope class.

    :param time_start: Time the envelope is starting (None for -infinity).
    :param time_stop: Time the envelope is stopping (None for +infinity).
    :param period: Envelope period (None for non-periodic envelope, 0. for constant envelope).
    """

    def __init__(
            self,
            time_start: Optional[float] = None,
            time_stop: Optional[float] = None,
            period: Optional[float] = None
    ):
        self.time_start = time_start
        self.time_stop = time_stop
        self.period = period

    def is_in_bounds(self, t: float) -> bool:
        """Checks if time point is in envelope acting bounds.

        :param t: Time point.
        :return: True if the time value is inside envelope bounds.
        """
        if self.time_start is not None and t < self.time_start:
            return False
        if self.time_stop is not None and t > self.time_stop:
            return False
        return True

    def __call__(self, t: float) -> complex:
        """Returns the envelope value at specific time point.

        :param t: Time point.
        :return: Envelope value.
        """
        return self._get_value(t) if self.is_in_bounds(t) else 0.

    def drv(self, t: float) -> complex:
        """Returns the envelope derivative at specific time point.

        :param t: Time point.
        :return: Derivative value.
        """
        return self._get_derivative(t) if self.is_in_bounds(t) else 0.

    @abstractmethod
    def _get_value(self, t: float) -> complex:
        pass

    @abstractmethod
    def _get_derivative(self, t: float) -> complex:
        pass

    def conj(self):
        """Returns the complex conjugate envelope"""
        return ConjugateEnvelope(self)

class ConjugateEnvelope(Envelope):
    """Complex conjugate envelope.

    :param parent_envelope: Envelope to conjugate.
    """

    def __init__(self, parent_envelope: Envelope):
        super().__init__(
            time_start=parent_envelope.time_start,
            time_stop=parent_envelope.time_stop,
            period=parent_envelope.period
        )

        self.__parent_envelope = parent_envelope

    def _get_value(self, t: float) -> complex:
        parent_value = self.__parent_envelope._get_value(t)
        if isinstance(parent_value, float):
            return parent_value
        else:
            return parent_value.conjugate()

    def _get_derivative(self, t: float) -> complex:
        parent_value = self.__parent_envelope._get_derivative(t)
        if isinstance(parent_value, float):
            return parent_value
        else:
            return parent_value.conjugate()

    def conj(self):
        return self.__parent_envelope

class ConstantEnvelope(Envelope):
    """Constant value envelope.

    :param value: Envelope value.
    :param time_start: Time the envelope is starting (None for -infinity).
    :param time_stop: Time the envelope is stopping (None for +infinity).
    """

    def __init__(self, value: complex, time_start: float = None, time_stop: float = None):
        super().__init__(time_start, time_stop, 0.)
        self.value = value

    def _get_value(self, t: float) -> complex:
        return self.value

    def _get_derivative(self, t: float) -> complex:
        return 0.

    def smoothen(self, front_width: float):
        """Returns the smooth version of the envelope, when it starts and ends smoothly.

        :param front_width: The front width of the envelope.
        :return: New envelope with smooth bounds.
        """
        return PulseEnvelope(front_width, self.value, self.time_start, self.time_stop)

class CustomEnvelope(Envelope):
    """Custom envelope

    :param fun: Function handler to calculate the envelope value at specific time point.
    :param fun_derivative: Function handler to calculate the envelope time derivative value at specific time point.
    :param fun_dt: Delta time to compute the envelope derivative using the finite difference.
    Is used when `fun_derivative` is None.
    :param time_start: Time the envelope is starting (None for -infinity).
    :param time_stop: Time the envelope is stopping (None for +infinity).
    :param period: Envelope period (None for non-periodic envelope, 0. for constant envelope).
    """

    def __init__(
            self,
            fun: Callable[[float], complex],
            fun_derivative: Callable[[float], complex] = None,
            fun_dt: float = None,
            time_start: Optional[float] = None,
            time_stop: Optional[float] = None,
            period: Optional[float] = None
    ):
        super().__init__(time_start, time_stop, period)
        self.fun = fun
        if fun_derivative is None:
            fun_derivative = lambda t: (fun(t + fun_dt) - fun(t)) / fun_dt
        self.fun_derivative = fun_derivative

    def _get_value(self, t: float) -> complex:
        return self.fun(t)

    def _get_derivative(self, t: float) -> complex:
        return self.fun_derivative(t)

class ProductEnvelope(Envelope):
    """The product of multiple envelopes.

    :param multipliers: Product multipliers.
    :param time_start: Time the envelope is starting (None to compute from the multipliers).
    :param time_stop: Time the envelope is stopping (None to compute from the multipliers).
    :param period: Envelope period (None to compute from the multipliers).
    """

    def __init__(
            self,
            multipliers: List[Envelope],
            time_start: Optional[float] = None,
            time_stop: Optional[float] = None,
            period: Optional[float] = None
    ):
        if time_start is None:
            time_start_list = [envelope.time_start for envelope in multipliers if envelope.time_start is not None]
            time_start = None if len(time_start_list) == 0 else max(time_start_list)
        if time_stop is None:
            time_stop_list = [envelope.time_stop for envelope in multipliers if envelope.time_stop is not None]
            time_stop = None if len(time_stop_list) == 0 else min(time_stop_list)
        if period is None:
            period = get_common_period(multipliers)
        super().__init__(time_start, time_stop, period)
        self.multipliers = multipliers

    def _get_value(self, t: float) -> complex:
        return complex(np.prod([envelope(t) for envelope in self.multipliers], dtype=complex))

    def _get_derivative(self, t: float) -> complex:
        drv = complex(0.)
        for idx, envelope in enumerate(self.multipliers):
            m1 = complex(np.prod([e(t) for e in self.multipliers[:idx] + self.multipliers[idx + 1:]], dtype=complex))
            drv += m1 * envelope.drv(t)
        return drv

class SumEnvelope(Envelope):
    """The sum of multiple envelopes.

    :param terms: Sum terms.
    :param time_start: Time the envelope is starting (None to compute from the terms).
    :param time_stop: Time the envelope is stopping (None to compute from the terms).
    :param period: Envelope period (None to compute from the terms).
    """

    def __init__(
            self,
            terms: List[Envelope],
            time_start: Optional[float] = None,
            time_stop: Optional[float] = None,
            period: Optional[float] = None
    ):
        if time_start is None:
            time_start_list = [envelope.time_start for envelope in terms]
            time_start = None if None in time_start_list else min(time_start_list)
        if time_stop is None:
            time_stop_list = [envelope.time_stop for envelope in terms]
            time_stop = None if None in time_stop_list else max(time_stop_list)
        super().__init__(time_start, time_stop, period)

        self.terms = terms

    def _get_value(self, t: float) -> complex:
        return sum(envelope(t) for envelope in self.terms)

    def _get_derivative(self, t: float) -> complex:
        return sum(envelope.drv(t) for envelope in self.terms)

    def conj(self):
        return SumEnvelope(
            terms=[term.conj() for term in self.terms],
            time_start=self.time_start,
            time_stop=self.time_stop,
            period=self.period
        )

    def expand(self):
        """Expands the envelope to it terms.

        :return: List of terms.
        """
        return self.terms

class PulseEnvelope(SumEnvelope):
    """Smooth pulse envelope.

    :param front_width: The pulse front width.
    :param amplitude: The pulse amplitude.
    :param time_start: Time the envelope is starting (None for -infinity).
    :param time_stop: Time the envelope is stopping (None for +infinity).
    """

    def __init__(
            self,
            front_width: float,
            amplitude: complex = 1.,
            time_start: Optional[float] = None,
            time_stop: Optional[float] = None
    ):
        super().__init__(self.get_terms(front_width, amplitude, time_start, time_stop))

    @staticmethod
    def get_terms(
            front_width: float,
            amplitude: complex = 1.,
            time_start: Optional[float] = None,
            time_stop: Optional[float] = None
    ):
        """Compute the terms of a smooth pulse envelope.

        :param front_width: The pulse front width.
        :param amplitude: The pulse amplitude.
        :param time_start: Time the envelope is starting (None for -infinity).
        :param time_stop: Time the envelope is stopping (None for +infinity).
        :return: List of envelope terms.
        """
        if time_start is not None and time_stop is not None:
            assert time_stop - time_start > 10 * front_width, "Pulse length must be larger than the 10 widths"

        terms = []

        if time_start is not None and front_width > 0.:
            terms.append(CustomEnvelope(
                fun=lambda t: amplitude * norm.cdf(t, loc=time_start, scale=front_width),
                fun_derivative=lambda t: amplitude * norm.pdf(t, loc=time_start, scale=front_width),
                time_start=time_start - 5 * front_width,
                time_stop=time_start + 5 * front_width
            ))

        terms.append(ConstantEnvelope(
            value=amplitude,
            time_start=None if time_start is None else time_start + 5 * front_width,
            time_stop=None if time_stop is None else time_stop - 5 * front_width
        ))

        if time_stop is not None and front_width > 0.:
            terms.append( CustomEnvelope(
                fun=lambda t: amplitude * (1. - norm.cdf(t, loc=time_stop, scale=front_width)),
                fun_derivative=lambda t: -amplitude * norm.pdf(t, loc=time_stop, scale=front_width),
                time_start=time_stop - 5 * front_width,
                time_stop=time_stop + 5 * front_width
            ))

        return terms

class PeriodicEnvelope(Envelope):
    """Periodic envelope: `A * exp(-1j*w*t + p) + b`.

    :param amplitude: Envelope amplitude `A`.
    :param frequency: Envelope frequency `w`.
    :param phase: Envelope phase `p`.
    :param shift: Envelope shift `b`.
    :param time_start: Time the envelope is starting (None for -infinity).
    :param time_stop: Time the envelope is stopping (None for +infinity).
    """

    def __init__(
            self,
            amplitude: float,
            frequency: float,
            phase: Optional[float] = 0.,
            shift: Optional[float] = 0.,
            time_start: Optional[float] = None,
            time_stop: Optional[float] = None
    ):
        period = 2 * np.pi / frequency
        if time_start is not None and time_stop is not None and period > time_stop - time_start:
            period = None

        super().__init__(time_start=time_start, time_stop=time_stop, period=period)

        self.amplitude = amplitude
        self.frequency = frequency
        self.phase = phase
        self.shift = shift

    def __new__(
            cls,
            amplitude: float,
            frequency: float,
            phase: Optional[float] = 0.,
            shift: Optional[float] = 0.,
            time_start: Optional[float] = None,
            time_stop: Optional[float] = None
    ):
        if frequency == 0:
            value = shift + amplitude * (np.cos(phase) + 1j * np.sin(phase))
            return ConstantEnvelope(value, time_start, time_stop)
        return super().__new__(cls)

    def _get_value(self, t: float) -> complex:
        phase = self.frequency * t - self.phase
        return self.shift + self.amplitude * (np.cos(phase) - 1j * np.sin(phase))

    def _get_derivative(self, t: float) -> complex:
        phase = self.frequency * t - self.phase
        return self.amplitude * self.frequency * (-np.sin(phase) - 1j * np.cos(phase))

    def to_window(self, time_start: Optional[float] = None, time_stop: Optional[float] = None):
        """Crops the periodic envelope to specific window.

        :param time_start: Time the envelope is starting (None for -infinity).
        :param time_stop: Time the envelope is stopping (None for +infinity).
        :return: New instance of the envelope with specified window.
        """
        return self.__class__(
            amplitude=self.amplitude,
            frequency=self.frequency,
            phase=self.phase,
            shift=self.shift,
            time_start=time_start,
            time_stop=time_stop
        )

    def smoothen(self, front_width: float):
        """Returns the smooth version of the envelope, when it starts and ends smoothly.

        :param front_width: The front width of the envelope.
        :return: New envelope with smooth bounds.
        """
        pulse_envelope = PulseEnvelope(front_width, time_start=self.time_start, time_stop=self.time_stop)
        return SumEnvelope([
            ProductEnvelope([self.to_window(term.time_start, term.time_stop), term])
            for term in pulse_envelope.terms
        ])

def get_common_period(envelopes: List[Envelope]) -> Optional[float]:
    """Computes the common period of a number of envelopes.

    :param envelopes: List of envelopes.
    :return: Common period (None for non-periodic envelope, 0. for constant envelope).
    """
    periods = [envelope.period for envelope in envelopes]
    if None in periods:
        return None

    non_zero_periods = [period for period in periods if period > 0.]
    if len(non_zero_periods) == 0:
        return 0.

    period = non_zero_periods[0]

    if len(non_zero_periods) == 1:
        return period

    for period_j in non_zero_periods[1:]:
        q1, _ = Fraction.from_float(period_j / period).limit_denominator(1000).as_integer_ratio()
        period = q1 * period

    for envelope in envelopes:
        if envelope.time_start is not None:
            t0 = envelope.time_start
        elif envelope.time_stop is not None:
            t0 = envelope.time_stop - period
        else:
            t0 = 0.
        if abs(envelope(t0 + period) - envelope(t0)) > 1e-6:
            warn("Failed to approximate envelope period. Use non-periodic evolution.")
            return None
    return period