#!/usr/bin/env python
# encoding: utf-8
from argparse import Namespace
import sys
import os.path
from math import log, log2
from typing import List, Optional, Union, Dict, Callable
from random import SystemRandom as random_number_generator, randrange as random_range
from re import compile

from xkcd_phrase.lib.xkcd_default import (
    DEFAULT_WORDFILE,
    MIN_LENGTH,
    MAX_LENGTH,
    VALID_CHARS,
    SPECIAL_CHARS
)

from xkcd_phrase.lib.case import (CASE_METHODS)

def locate_wordfile(wordfile: str = None) -> Optional[str]:
    """
    Locate a wordfile from provided name/path. Return a path to wordfile
    either from dictionary directory, the provided path or use a default.
    """
    common_word_files = []
    dictionary_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "dictionary")

    if wordfile is not None:
        ''' wordfile can be in dictionary dir or provided as a complete path '''
        common_word_files.append(os.path.join(dictionary_dir, wordfile))
        common_word_files.append(os.path.expanduser(wordfile))
    else:
        common_word_files.extend(
            [
                os.path.join(dictionary_dir, DEFAULT_WORDFILE),
                "/usr/share/cracklib/cracklib-small",
                "/usr/share/dict/cracklib-small",
                "/usr/dict/words",
                "/usr/share/dict/words",
            ]
        )
    wfilecheck = False
    length = len(common_word_files)
    count = 0
    while wfilecheck == False:
        for wfile in common_word_files:
            count += 1
            if os.path.isfile(wfile):
                wfilecheck = True
                finalwfile = wfile
                break
            else:
                if count == length:
                    wfilecheck = True

    return finalwfile if "finalwfile" in locals() else None

def choose_words(wordlist: List[str], num_words: int, acrostic: str) -> List[str]:
    """
    Choose num_words randomly from wordlist, or, 
    choose random words based on the provided acrostic
    """
    if not acrostic:
        return [random_number_generator().choice(wordlist) for i in range(num_words)]
    else:
        worddict = wordlist_to_worddict(wordlist)
        return find_acrostic(acrostic, worddict)       

def set_case(
    words: List[str], method: str = "lower", testing: bool = False
) -> Union[Dict[str, List[str]], List[str]]:
    """
    Perform capitalization on some or all of the strings in `words`.
    Default method is "lower".
    Args:
        words (list):   word list generated by `choose_words()`.
        method (str):   one of {"alternating", "upper", "lower",
                        "random"}.
        testing (bool): only affects method="random".
                        If True: the random seed will be set to each word
                        prior to choosing True or False before setting the
                        case to upper. This way we can test that random is
                        working by giving different word lists.
    """
    if (method == "random") and (testing):
        return CASE_METHODS[method](words, testing=True)
    else:
        return CASE_METHODS[method](words)


def generate_wordlist(
    wordfile: str, min_length: int = MIN_LENGTH, max_length: int = MAX_LENGTH, valid_chars: str = VALID_CHARS) -> List[str]:
    """
    Generate a word list from either a kwarg wordfile, or a system default
    valid_chars is a regular expression match condition (default - all chars)
    """

    actualwordfile = locate_wordfile(wordfile)
    assert actualwordfile is not None
    ''' deduplicate, just in case '''
    words = set()

    regexp = compile("^{0}{{{1},{2}}}$".format(valid_chars, min_length, max_length))
    ''' read words from file into wordlist '''
    with open(actualwordfile, encoding="utf-8") as wlf:
        for line in wlf:
            thisword = line.strip()
            if regexp.match(thisword) is not None:
                words.add(thisword)

    return list(sorted(words))  


def wordlist_to_worddict(wordlist):
    """
    Takes a wordlist and returns a dictionary keyed by the first letter of
    the words. Used for acrostic passphrase generation
    """

    worddict = {}

    ''' Maybe should be a defaultdict, but this reduces dependencies '''
    for word in wordlist:
        try:
            worddict[word[0]].append(word)
        except KeyError:
            worddict[word[0]] = [word, ]

    return worddict


def find_acrostic(acrostic, worddict):
    """
    Constrain choice of words to those beginning with the letters of the
    given word (acrostic).
    Second argument is a dictionary (output of wordlist_to_worddict)
    """

    words = []

    for letter in acrostic:
        try:
            words.append(random_number_generator().choice(worddict[letter.lower()]))
        except KeyError:
            sys.stderr.write("No words found starting with " + letter + "\n")
            sys.exit(1)
    return words


def special_char_sub(words: List[str], num_words: int, special_char_num: int, delimiter: str, valid_delim: str) -> List[str]:
    """
    Randomly substitute special_char_num of Special chracters into the 
    selected words.  This is done by randomly picking one word per special_char_num
    and then randomly selecting a leter in the word and substituting a random special 
    character from DEFAULT_DELIMITERS
    """
    
    """
    Randomly pick words up to special_char_num.  Create a list (index) of those 
    to do a substitution and then sort the list
    """
    words_to_change = list()
    i=0
    while i in range(special_char_num):
        no_to_add = random_range(num_words)
        check_no = words_to_change.count(no_to_add)
        if check_no == 0:
            words_to_change.append(no_to_add)
            i+=1
    words_to_change.sort()
    
    """
    With the sorted list, randomly choose a character position
    Treat the words as an array and remove the character and replace it 
    with a randomly selected character from DEFAULT_DELIMITERS
    """ 
    for j in range(special_char_num):
        substitute = ""
        word_change = list(words[words_to_change[j]])
        word_change_len = len(word_change)
        char_to_sub = random_range(word_change_len)
        sub_char = delimiter
        while sub_char == delimiter:
            sub_char = random_number_generator().choice(valid_delim)
            if (sub_char == "%%") :
                sub_char = "%"
        word_change.pop(char_to_sub)
        word_change.insert(char_to_sub, sub_char)
 
        """
        Remove the original word and replace it with the substitute
        Retaining word order is important if this has come from
        an acrostic
        """
        words.pop(words_to_change[j])
        words.insert(words_to_change[j], substitute.join(word_change))
    return words


def generate_random_padding_numbers(numeric_char_num: int) -> int:
    """
    Get random numbers to append to passphrase
    """
    num_str=[]
    for i in range(numeric_char_num):
        num_str.append(str(random_number_generator().randint(0, 9)))
    return(''.join(num_str))

    """
    randmin = pow(10, numeric_char_num - 1)
    randmax = pow(100, numeric_char_num - 1)
    return random_number_generator().randint(a=randmin, b=randmax)
    """

def generate_random_padding_chars(special_char_num: int, delimiter: str) -> int:
    """
    Get random characters to append to passphrase
    """
    special_string = ""
    for k in range(special_char_num):
        sub_char = delimiter
        while sub_char == delimiter:
            sub_char = random_number_generator().choice(list(SPECIAL_CHARS.values()))
            if (sub_char == "%%") :
                sub_char = "%"
        special_string = special_string + sub_char
    return special_string
    

def padding_digits_sub(words: List[str], num_words: int, numeric_char_num: int) -> List[str]:
    """
    Randomly substitute numeric_char_num of numerical chracters into the 
    selected words.  This is done by randomly picking one word per numeric_char_num
    and then randomly selecting a leter in the word and substituting a random numerical 
    character
    """
    
    """
    Randomly pick words up to special_char_num.  Create a list (index) of those 
    to do a substitution and then sort the list
    """
    words_to_change = list()
    i=0
    while i in range(numeric_char_num):
        no_to_add = random_range(num_words)
        check_no = words_to_change.count(no_to_add)
        if check_no == 0:
            words_to_change.append(no_to_add)
            i+=1
    words_to_change.sort()
    
    """
    With the sorted list, randomly choose a character position
    Treat the words as an array and remove the character and replace it 
    with a randomly selected character from DEFAULT_DELIMITERS
    """ 
    for j in range(numeric_char_num):
        substitute = ""
        word_change = list(words[words_to_change[j]])
        word_change_len = len(word_change)
        char_to_sub = random_range(word_change_len)
        sub_char = str(random_number_generator().randint(0, 9))
        word_change.pop(char_to_sub)
        word_change.insert(char_to_sub, sub_char)
 
        """
        Remove the original word and replace it with the substitute
        Retaining word order is important if this has come from
        an acrostic
        """
        words.pop(words_to_change[j])
        words.insert(words_to_change[j], substitute.join(word_change))
    return words

def verbose_entropy(phrase, 
                      case, 
                      valid_chars, 
                      valid_delim, 
                      delim,
                      numeric_char_num, 
                      special_char_num,
                      )-> list:
    """
    Some complexity
    """
    
    """
    Identify from valid_chars the number of possible characters that could be used 
    """    
    poss_valid_char = len(valid_chars)
    """
    If just lowercase or just upper case then leave as is,  
    otherwise the choice is double
    """ 
    if ((case != 'lower') and (case!= 'upper')):
        poss_valid_char = poss_valid_char * 2
    
    char_string = "\t{0} alphabetic characters\n".format(poss_valid_char)
    
    """
    Deterimine possible number of numerical digits in the set
    """
    poss_numeric_char = 0
    
    if (numeric_char_num > 0):
        poss_numeric_char = 10
        numeric_string = "\t{0} numeric characters\n".format(poss_numeric_char)
    else:
        numeric_string = "\tNo numeric characters were used\n"

    """
    Identify from valid_delim the number of possible special characters that could be used 
    """    
    poss_special_char = 0

    if (special_char_num > 0):
        poss_special_char = len(valid_delim) 

    """
    Acounting for the case where no random characters are being used but a delimiter is
    """
    if ((poss_special_char == 0) and (delim != "")):
        poss_special_char = 1
    
    if (poss_special_char > 0) :
        special_string = "\t{0} special characters\n".format(poss_special_char)
    else:
        special_string = "\tNo special characters were used\n"
          
    possible_symb = poss_valid_char + poss_numeric_char + poss_special_char 
    symbol_string = (
        "\nThe total possible number of symbol choices in the phrase is"
        "\n{0} possible symbols comprising:\n"
        .format(possible_symb)
        )
    
    phrase_len = len(phrase)
    phrase_string = "The passphrase length is {0}.\n".format(phrase_len)
    
    entropy = round(log2(possible_symb**phrase_len),2)
    entropy_string = (
        "\nThe phrase length is {0} with the entropy of the phrase is calculated as:\n\t"
        "log2(possible_symb ({1}) ^ phrase_len ({0})) = {2}\n"
        "\nThe phrase is: {3}"
        .format(phrase_len, possible_symb, entropy, phrase)
    )
           
    return(list((symbol_string, char_string, numeric_string, special_string, entropy_string)))

