"""This code defines the layout, functions, and callbacks for the Command web
page.

AUTHOR        :  Brian Ha (summer intern)
MENTOR        :  Gene Massion
MBARI TEAM    :  Chemical Sensor Group
PROJECT       :  Coastal Profiling Floats (CPF)
DATE CREATED  :  2018-06-29
LAST REVISION :  2018-08-02
"""

# Python Standard Library Imports
# =============================================================================
from datetime import datetime
import smtplib
import email.mime.text
import email.mime.multipart
import email.mime.application


# 3rd Party Library Imports
# =============================================================================
import dash
import dash_core_components as dcc
import dash_html_components as html


# CPF MCS-Specific Imports
# =============================================================================
from gui.app import app
import gui.utils as utils
import gui.css as css


# Constants
# =============================================================================
from cpf_constants import SENDER, RECIPIENT_1, RECIPIENT_2
from cpf_constants import COMMAND_SBD_FILE_NAME
from cpf_constants import SMTP_HOST_NAME, SMTP_PORT_NUMBER
from cpf_constants import MIN_PROFILES, MAX_PROFILES
from cpf_constants import MIN_TIMEOUT_HOURS, MAX_TIMEOUT_HOURS
from cpf_constants import MIN_PARK_TIME_MINS, MAX_PARK_TIME_MINS
from cpf_constants import MIN_PARK_PRESSURE_DBAR, MAX_PARK_PRESSURE_DBAR


# Functions
# =============================================================================
def send_command_sbd(cpf_imei, command_string, recipient):
    '''Emails a command string in an attachment to a CPF based on IMEI number.'''

    # Create the email message
    msg = email.mime.multipart.MIMEMultipart()
    msg['Subject'] = str(cpf_imei)
    msg['From'] = SENDER
    msg['To'] = recipient

    # Convert the command string to a bytes object
    encoded_command = str(command_string).encode()

    # Create and attach the attachment
    attachment = email.mime.application.MIMEApplication(encoded_command)
    attachment.add_header('Content-Disposition', 'attachment', filename=COMMAND_SBD_FILE_NAME)
    msg.attach(attachment)

    # Send the email
    smtpObj = smtplib.SMTP(SMTP_HOST_NAME, SMTP_PORT_NUMBER)
    smtpObj.sendmail(SENDER, recipient, msg.as_string())


# Page Layout
# =============================================================================
layout = html.Div([
    utils.nav_bar,
    html.H1('SBD Command Builder', style=css.content_heading_style),

    html.Div([
        html.H4('Select a CPF...', style={'color': 'white'}),

        dcc.Dropdown(
            id='cpf_selector',
            options=utils.cpf_dropdown
        ),

        html.H4('Select a command...', style={'color': 'white'}),

        html.Div([
            html.Div([
                dcc.RadioItems(
                    id='command_options',
                    options=[
                        {'label': 'GO', 'value': 'GO'},
                        {'label': 'RECOVERY TRUE', 'value': 'RECOVERYTRUE'},
                        {'label': 'RECOVERY FALSE', 'value': 'RECOVERYFALSE'},
                        {'label': 'MAX PROFILES', 'value': 'MAXPROFILES'},
                        {'label': 'MISSION TIMEOUT [HOURS]', 'value': 'MISSIONTIMEOUTHOURS'},
                        {'label': 'PARK TIME [MINUTES]', 'value': 'PARKTIMEMINUTES'},
                        {'label': 'PARK PRESSURE [DBAR]', 'value': 'PARKPRESSUREDBAR'},
                    ],
                    labelStyle={'color': 'white', 'margin': 25, 'textAlign': 'left'}
                ),
            ], style={'display': 'inline-block', 'textAlign:': 'left', 'marginRight': 25}),
            html.Div([
                dcc.Input(
                    type='text',
                    id='max_profiles_input',
                    style=css.command_value_input_style
                ),
                html.Br(),
                dcc.Input(
                    type='text',
                    id='mission_timeout_input',
                    style=css.command_value_input_style
                ),
                html.Br(),
                dcc.Input(
                    type='text',
                    id='park_time_input',
                    style=css.command_value_input_style
                ),
                html.Br(),
                dcc.Input(
                    type='text',
                    id='park_pressure_input',
                    style=css.command_value_input_style
                ),
                html.Br(),
            ], style={'display': 'inline-block', 'color': 'white', 'paddingTop': 50}),
        ], style={'textAlign': 'center'}),

        html.P('', id='input_error_msg', style=css.range_input_error_msg_style),

        html.Button('Append', id='append_button', style=css.enabled_append_button_style),

        html.Button('Clear', id='clear_button', style=css.clear_button_style),

        html.H4('Command Preview', style={'color': 'white'}),

        dcc.Textarea(
            id='command_preview',
            style={'width': '100%'},
            disabled='False',
            value='',
        ),
        html.Div([
            html.P(' ', id='user_confirmation_msg'),
            html.Button('Send', id='send_button', style=css.send_button_style),
        ], style={'textAlign': 'right'}),

        # These hidden divs are not displayed on the GUI.
        # They store values and are used like global variables.
        # The difference is that they reset to zero upon page refresh.
        html.Div(id='hidden_div_for_command_string', style={'display': 'none'}),
        html.Div(id='hidden_div_append_counter', style={'display': 'none'}),
        html.Div(id='hidden_div_clear_counter', style={'display': 'none'}),

    ], style={'margin': 'auto', 'width': '50vw'}),
], style={**css.content_page_style, **{'minHeight': '900px'}})


# Callbacks
# =============================================================================
@app.callback(
    dash.dependencies.Output('command_preview', 'value'),
    [dash.dependencies.Input('append_button', 'n_clicks'),
    dash.dependencies.Input('clear_button', 'n_clicks')],
    [dash.dependencies.State('hidden_div_append_counter', 'children'),
    dash.dependencies.State('hidden_div_clear_counter', 'children'),
    dash.dependencies.State('command_options', 'value'),
    dash.dependencies.State('max_profiles_input', 'value'),
    dash.dependencies.State('mission_timeout_input', 'value'),
    dash.dependencies.State('park_time_input', 'value'),
    dash.dependencies.State('park_pressure_input', 'value'),
    dash.dependencies.State('hidden_div_for_command_string', 'children')]
)
def update_command_preview(append_n_clicks, clear_n_clicks, append_counter,
                           clear_counter, command_option, max_profiles_input,
                           mission_timeout_input, park_time_input,
                           park_pressure_input, full_command):

    command_values = {
        'GO': None,
        'RECOVERYTRUE': None,
        'RECOVERYFALSE': None,
        'MAXPROFILES': max_profiles_input,
        'MISSIONTIMEOUTHOURS': mission_timeout_input,
        'PARKTIMEMINUTES': park_time_input,
        'PARKPRESSUREDBAR': park_pressure_input
    }

    if command_option is None:
        return

    command_value = command_values[command_option]

    if append_n_clicks != append_counter:

        if command_option in ['GO', 'RECOVERYTRUE', 'RECOVERYFALSE']:
            full_command = full_command + '${};'.format(command_option)
        else:
            full_command = full_command + '${},{};'.format(command_option, command_value)

    elif clear_n_clicks != clear_counter:
        full_command = ''

    return full_command


@app.callback(
    dash.dependencies.Output('hidden_div_for_command_string', 'children'),
    [dash.dependencies.Input('command_preview', 'value')]
)
def update_command_string(command_string):
    """This function stores the command string within the
    hidden_div_for_command_string."""
    return command_string


@app.callback(
    dash.dependencies.Output('user_confirmation_msg', 'children'),
    [dash.dependencies.Input('send_button', 'n_clicks')],
    [dash.dependencies.State('cpf_selector', 'value'),
    dash.dependencies.State('command_preview', 'value')]
)
def validate_sbd_command(send_n_clicks, cpf_imei, command_string):

    if send_n_clicks is None:
        return

    if cpf_imei is None:
        message = 'ERROR: You must select a CPF above.'
        return message

    if command_string in ['', None]:
        message = 'ERROR: The command cannot be empty. Please append a command before sending.'
        return message

    # Send to all recipients. As of 2018 Aug10 that is data@iridium.com and magene@mbari.org
    send_command_sbd(cpf_imei, command_string, RECIPIENT_1)
    send_command_sbd(cpf_imei, command_string, RECIPIENT_2)

    message = 'Success! The above command was sent at {}.'.format(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    return message


@app.callback(
    dash.dependencies.Output('user_confirmation_msg', 'style'),
    [dash.dependencies.Input('user_confirmation_msg', 'children')],
)
def make_submission_error_messages_red(message):
    if message.startswith('ERROR'):
        return css.error_msg_style
    else:
        return css.successful_msg_style


@app.callback(
    dash.dependencies.Output('input_error_msg', 'children'),
    [dash.dependencies.Input('command_options', 'value'),
     dash.dependencies.Input('max_profiles_input', 'value'),
     dash.dependencies.Input('mission_timeout_input', 'value'),
     dash.dependencies.Input('park_time_input', 'value'),
     dash.dependencies.Input('park_pressure_input', 'value')]
)
def display_range_input_error_message(command_option, max_profiles_input,
                           mission_timeout_input, park_time_input, park_pressure_input):

    if command_option == 'MAXPROFILES':
        if not utils.validate_int_range_input(max_profiles_input, MIN_PROFILES, MAX_PROFILES):
            error_msg_1 = 'ERROR: "{}" is not valid for {}. '.format(max_profiles_input, command_option)
            error_msg_2 = 'Input must be between {} and {}.'.format(MIN_PROFILES, MAX_PROFILES)
            return error_msg_1 + error_msg_2

    elif command_option == 'MISSIONTIMEOUTHOURS':
        if not utils.validate_int_range_input(mission_timeout_input, MIN_TIMEOUT_HOURS, MAX_TIMEOUT_HOURS):
            error_msg_1 = 'ERROR: "{}" is not valid for {}. '.format(mission_timeout_input, command_option)
            error_msg_2 = 'Input must be between {} and {}.'.format(MIN_TIMEOUT_HOURS, MAX_TIMEOUT_HOURS)
            return error_msg_1 + error_msg_2

    elif command_option == 'PARKTIMEMINUTES':
        if not utils.validate_int_range_input(park_time_input, MIN_PARK_TIME_MINS, MAX_PARK_TIME_MINS):
            error_msg_1 = 'ERROR: "{}" is not valid for {}. '.format(park_time_input, command_option)
            error_msg_2 = 'Input must be between {} and {}.'.format(MIN_PARK_TIME_MINS, MAX_PARK_TIME_MINS)
            return error_msg_1 + error_msg_2

    elif command_option == 'PARKPRESSUREDBAR':
        if not utils.validate_int_range_input(park_pressure_input, MIN_PARK_PRESSURE_DBAR, MAX_PARK_PRESSURE_DBAR):
            error_msg_1 = 'ERROR: "{}" is not valid for {}. '.format(park_pressure_input, command_option)
            error_msg_2 = 'Input must be between {} and {}.'.format(MIN_PARK_PRESSURE_DBAR, MAX_PARK_PRESSURE_DBAR)
            return error_msg_1 + error_msg_2
    else:
        return None


@app.callback(
    dash.dependencies.Output('append_button', 'disabled'),
    [dash.dependencies.Input('input_error_msg', 'children')],
)
def disable_append_button(message):
    if message is None:
        return False
    else:
        return True


@app.callback(
    dash.dependencies.Output('append_button', 'style'),
    [dash.dependencies.Input('append_button', 'disabled')],
)
def make_append_button_grey(disabled_status):
    if disabled_status:
        return css.disabled_append_button_style
    else:
        return css.enabled_append_button_style


@app.callback(
    dash.dependencies.Output('hidden_div_append_counter', 'children'),
    [dash.dependencies.Input('append_button', 'n_clicks')],
)
def update_append_counter(n_clicks):
    """This function stores the number of times the append button has been
    pressed within the hidden_div_append_counter."""
    return n_clicks


@app.callback(
    dash.dependencies.Output('hidden_div_clear_counter', 'children'),
    [dash.dependencies.Input('clear_button', 'n_clicks')],
)
def update_clear_counter(n_clicks):
    """This function stores the number of times the clear button has been
    pressed within the hidden_div_clear_counter."""
    return n_clicks
