import os
import base64
import re
import dash
from dash import dcc
from dash import html
from dash import callback_context
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
from input.interface import InputInterface
import input.publication
from verarbeitung.process_main import Processing
from dash.dependencies import Input, Output, State
import plotly.express as px
import dash_bootstrap_components as dbc # pip install dash-bootstrap-components



app = dash.Dash(__name__, external_stylesheets=[dbc.themes.SPACELAB])  #SPACELAB https://bootswatch.com/default/ for more themes)

# List of options when inputting data and generating the graph
additional_options = ['Update Automatically','Smart Input']

# Reads the contents of info_box.txt.
# They can later be displayed by pressing the corresponding button.
f = open('info_box.txt', 'r')
boxcontent = f.read()
f.close()

app.layout = html.Div([
html.Div(children=[
    # Layer 0: For the Header and Help Function(s)
    dbc.Button(
            'Show Info',
            id='collapse-button',
            className="me-1",
            color="primary",
            n_clicks=0,
        ),
        dbc.Collapse(
            dbc.Card(dbc.CardBody(html.Div(boxcontent, style={'whiteSpace': 'pre-line'}))),
            id='collapse',
            is_open=False,
        ),

    # Layer 1: For the string input
    dbc.Spinner(html.Div([
        "Input: ",
        # A simple box for inputting a string.
        # Value is transmitted upon pressing return or clicking out of the box.
        dcc.Input(id='string-input', value='', type='text',debounce=True,
        style={ "width": "400px"},
        ),
        
    ]),size="lg", color="primary", type="border", fullscreen=True,),
        # Layer 2: For file input and recursion depths
        html.Div([
        "References Depth: ",
        # Forward recursion. Values between 1 and 10 can be entered.
        dcc.Input(id='forward-depth',value='1',type='number',min='0',max='5',
        style={ "width": "50px"},
        ),
        " Cited-by Depth: ",
        # Backward recursion. Values between 1 and 10 can be entered.
        dcc.Input(id='backward-depth',value='1',type='number',min='0',max='5',
        style={"width": "50px"},
        ),
        # Upload box. Can be used via drag-and-drop or byclicking on it to open a file viewer.
        dbc.Spinner(dcc.Upload(
            id="file-input",
            children=html.Div(
            #Drag and drop or click to select a file to upload
                ["Drag and drop"]),
            style={
                "width": "400px",
                "height": "60px",
                "lineHeight": "60px",
                "borderWidth": "1px",
                "borderStyle": "dashed",
                "borderRadius": "5px",
                "textAlign": "center",
                "margin": "10px",
            }),size="lg", color="primary", type="border", fullscreen=True,),
    ]),
   
    # Layer 3: For the checklist, Remove-/Start-Buttons and error message
    html.Div([
        # All input DOIs are collected in this checklist.
        # It is initialized to avoid error messages.
        dcc.Checklist(id='input-checklist',options=[],
            labelStyle = dict(display='block'),value=[]),
        # Displays error message if 'Smart Input' is active.
        html.Div(id='input-err',style={'color':'red'}),
        # Clears the entire list.
        dbc.Button(id='clear-all-button',children='Clear All', color="primary", className="me-1",style={'display': 'inline-block'}),
        # Clear all selected elements.
        dbc.Button(id='clear-selected-button',children='Clear Selected', color="primary", className="me-1",style={'display': 'inline-block'}),
        # Starts the process that generates a graph.
        dbc.Button(id='start-button',children='Generate Graph', color="primary", className="me-1",style={'display': 'inline-block'})
    ]),
    # Layer 4: For additional Options
    html.Div([
        html.H4('Additional Options'),
        # A checklist of all additional options that are listed above.
        dcc.Checklist(id='additional-options',
            options=[{'label':k,'value':k} for k in additional_options],
            value=[],labelStyle = dict(display= 'block'))
        ]),
        
    ], style={'padding': 10, 'flex': 0.8}),

 html.Div(children=[
    # Layer 5: For the Graph and corresponding error messages
            dbc.Spinner(html.Div([
        html.Div(id='generate-graph-error',style={'color':'red'}),
        html.Iframe(
            src="assets/index.html",
            style={"height": "650px", "width": "100%"},
        ),
    ]),size="lg", color="primary", type="border", fullscreen=True,),
    ], style={'padding': 10, 'flex': 1.2})
], style={'display': 'flex', 'flex-direction': 'row'})

@app.callback(
    Output('input-checklist','options'),
    Output('input-checklist','value'),
    Output('string-input','value'),
    Output('input-err','children'),
    Input('string-input','value'),
    Input('clear-all-button','n_clicks'),
    Input('clear-selected-button','n_clicks'),
    Input('file-input','contents'),
    State('input-checklist','options'),
    State('input-checklist','value'),
    State('additional-options','value')
)
def update_input_checklist(input_value,btn1,btn2,filecontents,all_inputs,
        selected_inputs,additional_options):
    '''
    Most important callback function. Updates the checklist that holds all inputs.
    State of the checklist as input is needed so that previews entries are readded.
    string-input is required as Output to clear the input box after each input.
    Different actions are performed depending on which input triggered the callback.
    The value-attribute of input-checklist must be updates so that the values
    of deleted elements no longer appear in the list of selected elements.

    :param input_value: given by dcc.Input
    :type input_value: string
    :param btn1: signals pressing of clear-all-button
    :type btn1: int
    :param btn2: signals pressing of clear-selected-button
    :type btn2: int
    :param filecontents: the contents of an uploaded file
    :type filecontents: bit-string
    :param all_inputs: all labels and values from the checklist,
        regardless if they have been checked or not
    :type all_inputs: list of dictionaries with 2 entries each
    :param selected_inputs: values of all checked elements
    :type selected_inputs: list of strings
    :param addtitional_options: all checked additional options
    :type additional_options: list of strings
    '''
    # changed_id is used to determine which Input has triggered the callback
    changed_id = [p['prop_id'] for p in callback_context.triggered][0]

    # if clear-all-button was pressed:
    if 'clear-all-button' in changed_id:
        os.remove('assets/json_text.json')
        return list(),list(),'',''
    
    # if clear-selected-button was pressed:
    if 'clear-selected-button' in changed_id:
        all_inputs = [i for i in all_inputs if i['value'] not in selected_inputs]
        return all_inputs,list(),'',''
    
    # when a new element is added via dcc.Input
    if 'string-input' in changed_id:
        # Creates a list of previously added inputs to make sure nothing is added twice
        currValues = [x['value'] for x in all_inputs]
        if input_value not in currValues:

            # if 'Smart Input' is selected, the input will be checked for validity
            # and a more readable string will be returned
            if 'Smart Input' in additional_options:
                try:
                    # Attempts to call get_publication. If unsuccesful,
                    # the DOI is not added and an error message is returned
                    i = InputInterface()
                    pub = i.get_pub_light(input_value)
                except Exception as err:
                    return all_inputs,selected_inputs,'','{}'.format(err)
                # Creates a more readable string to display in the checklist
                rep_str = pub.contributors[0] + ',' + pub.journal + \
                        ',' + pub.publication_date
                # Makes sure not to add the same article with different links
                currLabels = [x['label'] for x in all_inputs]
                if rep_str not in currLabels:
                    all_inputs.append({'label':rep_str, 'value':input_value})
            
            # if 'Smart Input' is not selected, the input value is added as is,
            # without checking for validity.
            else:
                all_inputs.append({'label':input_value,'value':input_value})
        return all_inputs,selected_inputs,'',''
    
    # when a txt-file is uploaded
    if 'file-input.contents' in changed_id:
        if filecontents:
            # Skips the info portion that is added when a file is uploaded
            found = base64.b64decode(re.search(',(.+?)$', filecontents).group(1))
            # Returns the binary string into a proper text
            text = found.decode('utf-8')
            # Creates a list of inputs by splitting the lines
            list_of_inputs = (text.strip().split('\n'))
            CurrValues = [x['value'] for x in all_inputs]
            # For every line the same actions as for a single input are performed
            for input_value in list_of_inputs:
                if input_value not in CurrValues:
                    if 'Smart Input' in additional_options:
                        try:
                            i = InputInterface()
                            pub = i.get_pub_light(input_value)
                        except Exception as err:
                            return all_inputs,selected_inputs,'','{}'.format(err)
                        rep_str = pub.contributors[0] + ',' + pub.journal + \
                                ',' + pub.publication_date
                        currLabels = [x['label'] for x in all_inputs]
                        if rep_str not in currLabels:
                            all_inputs.append({'label':rep_str, 'value':input_value})
                    else:
                        all_inputs.append({'label':input_value,'value':input_value})
        return all_inputs,selected_inputs,'',''
    # when the programm is first started:
    # if this is not done, the input_checklist will be generated
    # with one element that contains an empty string
    if input_value == '':
        return list(),list(),'',''


@app.callback(
    Output('collapse', 'is_open'),
    [Input('collapse-button', 'n_clicks')],
    [State('collapse', 'is_open')],
)
def toggle_collapse(n, is_open):
    '''
    This callback shows and hides the (first) info-box by, checking how# often
    the button has been pressed. The text was loaded at the top.
    :param n_clicks: number of times show-info has been clicked.
    'type n_clicks: int
    '''
    if n:
        return not is_open
    return is_open


@app.callback(
    Output('generate-graph-error','children'),
    Input('start-button','n_clicks'),
    Input('input-checklist','options'),
    Input('forward-depth','value'),
    Input('backward-depth','value'),
    State('additional-options','value')
)
def generate_output(n_clicks,all_inputs,forward_depth,backward_depth,additional_options):
    '''
    Basic structure for a callback that generates an output. This is only a
    proof of concept and has noting to do with the intended output yet.

    :param n_clicks: how often has Generate Graph been clicked
    :type n_clicks: int
    :param all_inputs: all labels and values from the checklist,
        regardless if they have been checked or not
    :type all_inputs: list of dictionaries with 2 entries each
    :param forward_depth: forward recursion depth
    :type forward_depth: unsigned int
    :param backward_depth: backward recursion depth
    :type backward_depth: unsigned int
    :param additional_options: value of all selected additional options
    :type additional_options: list of strings
    '''
    changed_id = [p['prop_id'] for p in callback_context.triggered][0]
    if n_clicks is None:
        raise PreventUpdate
    elif 'Update Automatically' in additional_options \
            or 'start-button' in changed_id:
        input_links = [x['value'] for x in all_inputs]
        errors = Processing(input_links,int(forward_depth),int(backward_depth),'assets/json_text.json')
        if errors:
            message = ['The following inputs are invalid and were not used:']
            for error in errors:
                message.append(html.Br())
                message.append(error)
            message = html.P(message)
            #message = [html.P(error) for error in errors]
            return message

if __name__ == '__main__':
    app.run_server(debug=False)