#/u/GoldenSights
import bot3 as bot
import datetime
import praw
import random
import requests
import sqlite3
import string
import sys
import time
import traceback

USERAGENT = '''
/u/GoldenSights Usernames data collection:
Gathering the creation dates of user accounts for visualization.
More at https://github.com/voussoir/reddit/tree/master/Usernames
'''.replace('\n', ' ').strip()
    
sql = sqlite3.connect('D:\\git\\reddit\\usernames\\un.db')
cur = sql.cursor()
cur.execute('''
    CREATE TABLE IF NOT EXISTS users(
    idint INT,
    idstr TEXT,
    created INT,
    human TEXT,
    name TEXT,
    link_karma INT,
    comment_karma INT,
    total_karma INT,
    available INT,
    lastscan INT)
    ''')
cur.execute('CREATE INDEX IF NOT EXISTS userindex ON users(idint)')
cur.execute('CREATE INDEX IF NOT EXISTS index_users_available ON users(available)')
cur.execute('CREATE INDEX IF NOT EXISTS nameindex ON users(lowername)')
sql.commit()

# These numbers are used for interpreting the tuples that come from SELECT
SQL_USER_COLUMNS = [
    'idint',
    'idstr',
    'created',
    'human',
    'name',
    'link_karma',
    'comment_karma',
    'total_karma',
    'available',
    'lastscan',
    'lowername',
]

SQL_USER = {key:index for (index, key) in enumerate(SQL_USER_COLUMNS)}


AVAILABILITY = {True:'available', False:'unavailable', 'available':1, 'unavailable':0}
HEADER_FULL = '  ID            CREATED                  NAME             LINK     COMMENT      TOTAL            LAST SCANNED'
HEADER_BRIEF = '      LAST SCANNED       |   NAME'

MEMBERFORMAT_FULL = '{id:>6} {created} {username:<20} {link_karma:>9} {comment_karma:>9} ({total_karma:>10}) | {lastscan}'
MEMBERFORMAT_BRIEF = '{lastscan} | {username}'

MIN_LASTSCAN_DIFF = 86400 * 2000
# Don't rescan a name if we scanned it this many days ago

VALID_CHARS = string.ascii_letters + string.digits + '_-'

# If True, print the name of the user we're about to fetch.
# Good for debugging problematic users.
PREPRINT = False


print('Logging in.')
r = praw.Reddit(USERAGENT)
bot.login(r)


def allpossiblefromset(characters, length=None, minlength=None, maxlength=None):
    '''
    Given an iterable of characters, return a generator that creates every
    permutation of length `length`.

    If `minlength` and `maxlength` are both provided, all values of intermediate
    lengths will be generated
    '''

    if not (minlength is None or maxlength is None):
        for x in range(minlength, maxlength+1):
            for item in allpossiblefromset(characters, x):
                yield item
    elif length is None:
        raise ValueError('`length` must be provided if you arent using the min/max')
    else:
        endingpoint = len(characters) ** length

        characters = ''.join(sorted(list(set(characters))))

        for permutation in range(endingpoint):
            permutation = base36encode(permutation, alphabet=characters)
            l = len(permutation)
            if l < length:
                permutation = (characters[0] * (length-l)) + permutation
            yield permutation

def base36encode(number, alphabet='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'):
    """Converts an integer to a base36 string."""
    if not isinstance(number, (int)):
        raise TypeError('number must be an integer')
    base36 = ''
    sign = ''
    if number < 0:
        sign = '-'
        number = -number
    if 0 <= number < len(alphabet):
        return sign + alphabet[number]
    while number != 0:
        number, i = divmod(number, len(alphabet))
        base36 = alphabet[i] + base36
    return sign + base36

def base36decode(number):
    return int(number, 36)

def b36(i):
    if type(i) == int:
        return base36encode(i)
    if type(i) == str:
        return base36decode(i)

def check_old(available=None, threshold=86400):
    '''
    Update names in ascending order of their last scan
    available = False : do not include available names
                None  : do include available names
                True  : only include available names
    threshold = how long ago the lastscan must be.
    '''
    now = getnow()
    threshold = now - threshold
    assert available in (False, None, True)
    if available == False:
        query = 'SELECT name FROM users WHERE available = 0 AND lastscan < ? ORDER BY lastscan ASC'
    elif available == None:
        query = 'SELECT name FROM users WHERE lastscan < ? ORDER BY lastscan ASC'
    elif available == True:
        query = 'SELECT name FROM users WHERE available = 1 AND lastscan < ? ORDER BY lastscan ASC'
    cur.execute(query, [threshold])
    availables = cur.fetchall()
    for item in availables:
        process(item, quiet=True, noskip=True)

def count(validonly=False):
    if validonly:
        cur.execute('SELECT COUNT(*) FROM users WHERE idint IS NOT NULL AND available == 0')
    else:
        cur.execute('SELECT COUNT(*) FROM users')
    return cur.fetchone()[0]

def execit(*args, **kwargs):
    '''
    Allows another module to do stuff here using local names instead of qual names.
    '''
    exec(*args, **kwargs)

def fetchgenerator(cur):
    '''
    Create an generator from cur fetches so I don't
    have to use while loops for everything
    '''
    while True:
        fetch = cur.fetchone()
        if fetch is None:
            break
        yield fetch

def fetchwriter(cur, outfile, spacer1=' ', spacer2=None, brief=False):
    '''
    Write items from the current sql query to the specified file.

    If two spacers are provided, it will flip-flop between them
    on alternating lines to help readability.
    '''
    flipflop = True
    for item in fetchgenerator(cur):
        spacer = spacer1 if flipflop else spacer2
        if brief:
            item = memberformat_brief(item)
        else:
            item = memberformat_full(item)
        print(item, file=outfile)
        if spacer2 is not None:
            flipflop = not flipflop

def find(name, doreturn=False):
    '''
    Print the details of a username.
    '''
    f = getentry(name=name)
    if f:
        if doreturn:
            return f
        print_message(f)
    return None

def get_from_hot(sr, limit=None, submissions=True, comments=False, returnnames=False):
    '''
    Shortcut for get_from_listing, using /hot
    '''
    listfunction = praw.objects.Subreddit.get_hot
    return get_from_listing(sr, limit, listfunction, submissions, comments, returnnames)

def get_from_listing(sr, limit, listfunction, submissions=True, comments=True, returnnames=False):
    '''
    Get submission listings using one of PRAW's get methods
    and process those usernames

    `listfunction` would be praw.objects.Subreddit.get_new for example
    '''
    subreddit = r.get_subreddit(sr, fetch=sr != 'all')
    if limit is None:
        limit = 1000
    authors = set()
    if submissions is True:
        print('/r/%s, %d submissions' % (subreddit.display_name, limit))
        subreddit.lf = listfunction
        for item in subreddit.lf(subreddit, limit=limit):
            if item.author is not None:
                authors.add(item.author.name)
    if comments is True:
        print('/r/%s, %d comments' % (subreddit.display_name, limit))
        for item in subreddit.get_comments(limit=limit):
            if item.author is not None:
                authors.add(item.author.name)

    if returnnames is True:
        return authors

    try:
        process(authors)
    except KeyboardInterrupt:
        sql.commit()
        raise

def get_from_new(sr, limit=None, submissions=True, comments=True, returnnames=False):
    '''
    Shortcut for get_from_listing, using /new
    '''
    listfunction = praw.objects.Subreddit.get_new
    return get_from_listing(sr, limit, listfunction, submissions, comments, returnnames)

def get_from_top(sr, limit=None, submissions=True, comments=False, returnnames=False):
    '''
    Shortcut for get_from_listing, using /top?t=all
    '''
    listfunction = praw.objects.Subreddit.get_top_from_all
    return get_from_listing(sr, limit, listfunction, submissions, comments, returnnames)

def getentry(**kwargs):
    if len(kwargs) != 1:
        raise Exception("Only 1 argument please")
    kw = list(kwargs.keys())[0]
    if kw == 'idint':
        cur.execute('SELECT * FROM users WHERE idint=?', [kwargs[kw]])
    elif kw == 'idstr':
        cur.execute('SELECT * FROM users WHERE idstr=?', [kwargs[kw]])
    elif kw == 'name':
        cur.execute('SELECT * FROM users WHERE lowername=?', [kwargs[kw].lower()])
    else:
        return None
    return cur.fetchone()

def getnow(timestamp=True):
    now = datetime.datetime.now(datetime.timezone.utc)
    if timestamp:
        return now.timestamp()
    return now

def human(timestamp):
    day = datetime.datetime.utcfromtimestamp(timestamp)
    human = datetime.datetime.strftime(day, "%b %d %Y %H:%M:%S UTC")
    return human

def idlenew(subreddit='all', limit=100, sleepy=15):
    '''
    Infinitely grab the /new queue and process names, ignoring any
    exceptions. Great for processing while AFK.
    '''
    while True:
        try:
            get_from_new(subreddit, limit)
        except KeyboardInterrupt:
            raise
        except:
            traceback.print_exc()
        r._use_oauth = False
        time.sleep(sleepy)

def load_textfile(filename):
    '''
    Returns list of lines.
    See also `save_textfile`.
    '''
    f = open(filename, 'r')
    lines = [x.strip() for x in f.readlines()]
    f.close()
    return lines

def memberformat_brief(data):
    '''
    Shorter version of memberformat which I'm using for the "available" list.
    '''
    name = data[SQL_USER['name']]
    lastscan = data[SQL_USER['lastscan']]
    lastscan = human(lastscan)

    out = MEMBERFORMAT_BRIEF.format(lastscan=lastscan, username=name)
    return out

def memberformat_full(data):
    '''
    Given a data list, create a string that will
    become a single row in one of the show files.
    '''
    idstr = data[SQL_USER['idstr']]

    # Usernames are maximum of 20 chars
    name = data[SQL_USER['name']]

    created = data[SQL_USER['human']]
    created = created or ''
    link_karma = data[SQL_USER['link_karma']]
    comment_karma = data[SQL_USER['comment_karma']]
    total_karma = data[SQL_USER['total_karma']]
    if link_karma is None:
        link_karma = 'None'
        comment_karma = 'None'
        total_karma = 'None'
    else:
        link_karma = '{:,}'.format(link_karma)
        comment_karma = '{:,}'.format(comment_karma)
        total_karma = '{:,}'.format(total_karma)

    lastscan = data[SQL_USER['lastscan']]
    lastscan = human(lastscan)
    out = MEMBERFORMAT_FULL.format(
        id=idstr,
        created=created,
        username=name,
        link_karma=link_karma,
        comment_karma=comment_karma,
        total_karma=total_karma,
        lastscan=lastscan,
    )
    return out

def popgenerator(x):
    '''
    Create a generator which whittles away at the input
    list until there are no items left.
    This destroys the input list in-place.
    '''
    while len(x) > 0:
        yield x.pop()

def process(users, quiet=False, knownid='', noskip=False):
    '''
    Fetch the /u/ page for a user or list of users

    users :   A list of strings, each representing a username. Since reddit
               usernames must be 3 - 20 characters and only contain
               alphanumeric + "_-", any improper strings will be removed.
    quiet :   Silences the "x old" report at the end
    knownid : If you're processing a user which does not exist, but you know
               what their user ID was supposed to be, this will at least allow
               you to flesh out the database entry a little better.
    noskip :  Do not skip usernames which are already in the database.
    '''
    olds = 0
    if isinstance(users, list):
        users = list(set(users))

    if isinstance(users, str):
        users = [users]
    # I don't want to import types.GeneratorType just for one isinstance
    if type(users).__name__ == 'generator' or len(users) > 1:
        knownid=''
    users = userify_list(users, noskip=noskip, quiet=quiet)