import argparse
import json
import os
import readchar
import sys
import time
import readline

from subprocess import call, Popen, PIPE
from clint import resources
from clint.textui import puts, colored

targets = []
config_name = ''

# Time to sleep between transitions
TRANSITION_DELAY_TIME = 0.5

NUMBER_ENTRY_EXPIRE_TIME = 0.75


def main():
    global config_name

    # Check arguments
    parser = argparse.ArgumentParser(prog='sshmenu',
                                     description='A convenient tool for bookmarking '
                                                 'hosts and connecting to them via ssh.')
    parser.add_argument('-c', '--configname', default='config', help='Specify an alternate configuration name.')
    args = parser.parse_args()

    # Get config name
    config_name = '{configname}.json'.format(configname=args.configname)

    # First parameter is 'company' name, hence duplicate arguments
    resources.init('sshmenu', 'sshmenu')

    # If the config file doesn't exist, create an example config
    if resources.user.read(config_name) is None:
        example_config = {
            'targets': [
                {
                    'host': 'user@example-machine.local',
                    'friendly': 'This is an example target',
                    'options': []
                },
                {
                    'command': 'mosh',
                    'host': 'user@example-machine.local',
                    'friendly': 'This is an example target using mosh',
                    'options': []
                }
            ]
        }
        resources.user.write(config_name, json.dumps(example_config, indent=4))

    update_targets()
    display_menu()


def get_terminal_height():
    # Return height of terminal as int
    tput = Popen(['tput', 'lines'], stdout=PIPE)
    height, stderr = tput.communicate()

    return int(height)


def display_help():
    # Clear screen and show the help text
    call(['clear'])
    puts(colored.cyan('Available commands (press any key to exit)'))

    puts(' enter       - Connect to your selection')
    puts(' crtl+c | q  - Quit sshmenu')
    puts(' k (up)      - Move your selection up')
    puts(' j (down)    - Move your selection down')
    puts(' h           - Show help menu')
    puts(' c           - Create new connection')
    puts(' d           - Delete connection')
    puts(' e           - Edit connection')
    puts(' + (plus)    - Move connection up')
    puts(' - (minus)   - Move connection down')

    # Hang until we get a keypress
    readchar.readkey()


def connection_create():
    global config_name

    call(['clear'])
    puts(colored.cyan('Create new connection entry'))
    puts('')

    host = input('Hostname (user@machine): ')

    if host is '':
        puts('')
        puts('Nothing done')
        time.sleep(TRANSITION_DELAY_TIME)
        return

    friendly = input('Description []: ')
    command = input('Command [ssh]: ')
    options = input('Command Options []: ')

    # Set the defaults if our input was empty
    command = 'ssh' if command == '' else command
    options = [] if options == '' else options.split()

    # Append the new target to the config
    config = json.loads(resources.user.read(config_name))
    config['targets'].append({'command': command, 'host': host, 'friendly': friendly, 'options': options})

    # Save the new config
    resources.user.write(config_name, json.dumps(config, indent=4))
    update_targets()

    puts('')
    puts('New connection added')
    time.sleep(TRANSITION_DELAY_TIME)


def connection_edit(selected_target):
    global targets, config_name

    call(['clear'])
    puts(colored.cyan('Editing connection %s' % targets[selected_target]['host']))
    puts('')

    target = targets[selected_target]

    while True:
        host = input_prefill('Hostname: ', target['host'])
        if host is not '':
            break

    friendly = input_prefill('Description: ', target['friendly'])
    command = input_prefill('Command [ssh]: ', 'ssh' if not target.get('command') else target['command'])
    options = input_prefill('Options []: ', ' '.join(target['options']))

    # Set the defaults if our input was empty
    command = 'ssh' if command == '' else command
    options = [] if options == '' else options.split()

    # Delete the old entry insert the edited one in its place
    config = json.loads(resources.user.read(config_name))
    del config['targets'][selected_target]
    config['targets'].insert(selected_target,
                             {'command': command, 'host': host, 'friendly': friendly, 'options': options})

    resources.user.write(config_name, json.dumps(config, indent=4))
    update_targets()

    puts('')
    puts('Changes saved')
    time.sleep(TRANSITION_DELAY_TIME)


def connection_delete(selected_target):
    global targets, config_name

    call(['clear'])
    puts(colored.red('Delete connection entry for %s' % targets[selected_target]['host']))
    puts('')

    while True:
        response = input('Are you sure you want to delete this connection [yes|NO]: ').lower()

        if response == 'no' or response == 'n' or response == '':
            puts('')
            puts('Nothing done')
            break

        if response == 'yes':
            config = json.loads(resources.user.read(config_name))
            del config['targets'][selected_target]

            resources.user.write(config_name, json.dumps(config, indent=4))
            update_targets()

            puts('')
            puts('Connection deleted')
            break

    time.sleep(TRANSITION_DELAY_TIME)


def connection_move_up(selected_target):
    global config_name

    config = json.loads(resources.user.read(config_name))
    config['targets'].insert(selected_target - 1, config['targets'].pop(selected_target))

    resources.user.write(config_name, json.dumps(config, indent=4))
    update_targets()


def connection_move_down(selected_target):
    global config_name

    config = json.loads(resources.user.read(config_name))
    config['targets'].insert(selected_target + 1, config['targets'].pop(selected_target))

    resources.user.write(config_name, json.dumps(config, indent=4))
    update_targets()


def update_targets():
    global targets, config_name

    config = json.loads(resources.user.read(config_name))
    if 'targets' in config:
        targets = config['targets']


def display_menu():
    global targets

    # Save current cursor position so we can overwrite on list updates
    call(['tput', 'clear', 'sc'])

    # Keep track of currently selected target
    selected_target = 0

    # Support input of long numbers
    number_buffer = []

    # Store time of last number that was entered
    time_last_digit_pressed = round(time.time())

    # Get initial terminal height
    terminal_height = get_terminal_height()

    # Set initial visible target range.
    # Subtract 2 because one line is used by the instructions,
    # and one line is always empty at the bottom.
    visible_target_range = range(terminal_height - 2)

    while True:
        # Return to the saved cursor position
        call(['tput', 'clear', 'rc'])

        # We need at least one target for our UI to make sense
        num_targets = len(targets)
        if num_targets <= 0:
            puts(colored.red('Whoops, you don\'t have any connections defined in your config!'))
            puts('')
            puts('Press "c" to create a new connection')
        else:
            puts(colored.cyan('Select a target (press "h" for help)'))

            # Determine the longest host
            longest_host = -1
            longest_line = -1
            for index, target in enumerate(targets):
                length = len(target['host'])
                # Check host length
                if length > longest_host:
                    longest_host = length

            # Generate description and check line length
            for index, target in enumerate(targets):
                desc = target['host'].ljust(longest_host) + ' | ' + target['friendly']
                target['desc'] = desc
                line_length = len(desc)
                if line_length > longest_line:
                    longest_line = line_length

            # Recalculate visible targets based on selected_target
            if selected_target > max(visible_target_range):
                visible_start = selected_target - terminal_height + 3
                visible_end = selected_target + 1
                visible_target_range = range(visible_start, visible_end)
            elif selected_target < min(visible_target_range):
                visible_start = selected_target
                visible_end = selected_target + terminal_height - 2
                visible_target_range = range(visible_start, visible_end)

            # Make sure our selected target is not higher than possible
            # This can happen if you delete the last target
            selected_target = selected_target if selected_target < num_targets else 0

            # Used to pad out the line numbers so that we can keep everything aligned
            num_digits = len(str(num_targets))
            digits_format_specifier = '%' + str(num_digits) + 'd'

            # Print items
            for index, target in enumerate(targets):
                # Only print the items that are within the visible range.
                # Due to lines changing their position on the screen when scrolling,
                # we need to redraw the entire line + add padding to make sure all
                # traces of the previous line are erased.
                if index in visible_target_range:
                    line = (digits_format_specifier + '. %s ') % (index + 1, target['desc'].ljust(longest_line))
                    if index == selected_target:
                        puts(colored.green(' -> %s' % line))
                    else:
                        puts(colored.white('    %s' % line))

        # Hang until we get a keypress
        key = readchar.readkey()

        if key == readchar.key.UP or key == 'k' and num_targets > 0:
            # Ensure the new selection would be valid & reset number input buffer
            if (selected_target - 1) >= 0:
                selected_target -= 1
            number_buffer = []

        elif key == readchar.key.DOWN or key == 'j' and num_targets > 0:
            # Ensure the new selection would be valid & reset number input buffer
            if (selected_target + 1) <= (num_targets - 1):
                selected_target += 1
            number_buffer = []

        elif key == 'g':
            # Go to top & reset number input buffer
            selected_target = 0
            number_buffer = []

        elif key == 'G':
            # Go to bottom & reset number input buffer
            selected_target = num_targets - 1
            number_buffer = []

        # Check if key is a number
        elif key in map(lambda x: str(x), range(10)):
            current_time = time.time()
            if current_time - time_last_digit_pressed >= NUMBER_ENTRY_EXPIRE_TIME:
                number_buffer = []
            time_last_digit_pressed = current_time

            number_buffer += key
            new_selection = int(''.join(number_buffer))

            # If the new target is invalid, just keep the previously selected target instead
            if num_targets >= new_selection > 0:
                selected_target = new_selection - 1

        elif key == readchar.key.ENTER and num_targets > 0:
            # For cleanliness clear the screen
            call(['tput', 'clear'])

            target = targets[selected_target]

            # Check if there is a custom command for this target
            if 'command' in target.keys():
                command = target['command']
            else:
                command = 'ssh'

            # Arguments to the child process should start with the name of the command being run
            args = [command] + target.get('options', []) + [target['host']]
            try:
                # After this line, ssh will replace the python process
                os.execvp(command, args)
            except FileNotFoundError:
                sys.exit('Command not found: {commandname}'.format(commandname=command))

        elif key == 'h':
            display_help()

        elif key == 'c':
            connection_create()

        elif key == 'd' and num_targets > 0:
            connection_delete(selected_target)

        elif key == 'e' and num_targets > 0:
            connection_edit(selected_target)

        elif key == '-' and num_targets > 0:
            if selected_target < num_targets:
                connection_move_down(selected_target)
                selected_target += 1

        elif key == '+' and num_targets > 0:
            if selected_target > 0:
                connection_move_up(selected_target)
                selected_target -= 1

        elif key == readchar.key.CTRL_C or key == 'q':
            exit(0)


def input_prefill(prompt, text):
    def hook():
        readline.insert_text(text)
        readline.redisplay()

    readline.set_pre_input_hook(hook)
    result = input(prompt)
    readline.set_pre_input_hook()
    return result