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

app = dash.Dash(__name__)

# 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([
    # Layer 0: For the Header and Help Function(s)
    html.Div([
        html.Button(id='show-info',children='Show Info',n_clicks=0),
        html.Div(id='info-box')
    ]),
    # Layer 1: For all mandatory Inputs
    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='input-string', value='', type='text',debounce=True),
        # Forward recursion. Values between 1 and 10 can be entered.
        dcc.Input(id='forward-depth',value='1',type='number',min='1',max='10'),
        # Backward recursion. Values between 1 and 10 can be entered.
        dcc.Input(id='backward-depth',value='1',type='number',min='1',max='10'),
        # Upload box. Can be used via drag-and-drop or byclicking on it to open a file viewer.
        dcc.Upload(
            id="upload-data",
            children=html.Div(
                ["Drag and drop or click to select a file to upload."]),
            style={
                "width": "30%",
                "height": "60px",
                "lineHeight": "60px",
                "borderWidth": "1px",
                "borderStyle": "dashed",
                "borderRadius": "5px",
                "textAlign": "center",
                "margin": "10px",
            })
    ]),
    # Layer 2: For the checklist, Remove-/Start-Buttons and input-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.
        html.Button(id='clear-all-button',children='Clear All'),
        # Clear all selected elements.
        html.Button(id='clear-selected-button',children='Clear Selected'),
        # Starts the process that generates a graph.
        html.Button(id='start-button',children='Generate Graph')
    ]),
    # Layer 3: For additional Options (e.g. Topological Sort)
    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=[])
    ]),
    # Layer 4: For the Graph
    html.Div(
        [html.Iframe(
            src="assets/index.html",
            style={"height": "600px", "width": "100%"},
        ),
        html.Div(id='test-output')
    ])
])

@app.callback(
    Output('input-checklist','options'),
    Output('input-checklist','value'),
    Output('input-string','value'),
    Output('input-err','children'),
    Input('input-string','value'),
    Input('clear-all-button','n_clicks'),
    Input('clear-selected-button','n_clicks'),
    Input('upload-data','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.
    input-string 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 'input-string' 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
                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 'upload-data.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
                        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('info-box','children'),
    Input('show-info','n_clicks')
)
def show_hide_info_box(n_clicks):
    '''
    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_clicks % 2 == 0:
        return ''
    else:
        return html.Div(boxcontent, style={'whiteSpace': 'pre-line'})

@app.callback(
    Output('test-output','children'),
    Input('start-button','n_clicks'),
    Input('input-checklist','options'),
    Input('input-checklist','value'),
    Input('forward-depth','value'),
    Input('backward-depth','value'),
    State('additional-options','value')
)
def generate_output(n_clicks,all_inputs,selected_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 selected_inputs: values of all checked elements
    :type selected_inputs: list of strings
    :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]
        Processing(input_links,int(forward_depth),int(backward_depth),'assets/json_text.json')

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