#!/usr/bin/env python
from __future__ import print_function

import re
import os
import sys
import json
import base64
import string
import random
import argparse
import webbrowser
import requests

import tornado.web
import tornado.ioloop

try:
    import urlparse
    from urllib import urlencode
except:
    # Python3 imports
    import urllib.parse as urlparse
    from urllib.parse import urlencode

import pkg_resources
__version__ = pkg_resources.require("tmpr")[0].version

#=============================================================================
# web server functions and data
#=============================================================================
# set the root directory for data, by default we should only be working in the
# current directory where this python file lives
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'files')
CHARS = string.ascii_lowercase + ''.join(map(str, range(10)))

FAVICON = (
    "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAGBQT"
    "FRFooKnN8jxY5PVM7vvM7zwY5LUN8fxo4KnhInAuX2Py3172oZtSZ/k5ZJjOa7s6pxbP9LyTN"
    "jy66VYVtryo4Vmqoxo57Fit5djvp1lw6Jnzaps17Fu3bNt47Rp6a1csZJmx8KbeAAAAEJJREF"
    "UOMtjEBGVl5cXl5CUkpaRlRWTkxPi5+Xm4mTgYOXhY2YTEBRmGNEK2EcVEKuAiRgFjFRXIEOq"
    "ApZRBSQoAAAJHkuVEmG+EwAAAABJRU5ErkJggg=="
)

INDEX_CONTENT = \
string.Template("""
<link id="favicon" rel="shortcut icon" type="image/png"
 href="data:image/png;base64,$favicon">
<html>

<head>
<title>tmpr : file share</title>
<style media="screen" type="text/css">
.prebox {
    width: 640px;
    margin: auto;
    background-color: rgba(255, 255, 255, 0.5);
    padding: 10px;
}

.inputfile {
    width: 0.1px;
    height: 0.1px;
    opacity: 0;
    overflow: hidden;
    position: absolute;
    z-index: -1;
    cursor: pointer;
}

#label {
    cursor: pointer;
}

#upload {
    text-decoration: none;
    text-align: center;
    cursor: pointer;
    background-color: rgba(0,0,0,0.4);
    font-size: 26px;
    border: none;
    color: white;
    padding: 10px 20px;
}

#num {
    width: 50px;
    text-align: center;
    margin-top: 10px;
    font-size: 24px;
    background-color: rgba(0,0,0,0.4);
    border: none;
    color: white;
}

a {
    text-decoration: none;
    color: #444444;
}
</style>
</head>

<body style='background:url(data:image/png;base64,$favicon);background-size: 100% auto; margin: 30px;'>
<center><pre style='font-size:48px;margin:10px;'>TMPR : FILE SHARING</pre></center>
$content
</body></html>""")

FORM_CONTENT = """
<center><a href='/help'><h2>HELP</h2></a></center>
<br/><center>
<form enctype="multipart/form-data" action='/' method='post'>
<input type='file' id='filearg' name='filearg' class='inputfile'/><br/><!-- onchange="javascript:this.form.submit();"/><br/>-->
<label for='filearg' id='label'><svg width="512" height="512" viewBox="0 0 256 256" style='fill: rgba(0,0,0,0.4);'><g transform="scale(0.25 0.25)"><path d="M576 256l-128-128h-448v832h1024v-704h-448zM512 480l224 224h-160v256h-128v-256h-160l224-224z"></path></g></svg><span></span></label>
<br/><input id='upload' type='submit' value='Upload'>
<br/><input type='text' name='n' id='num' value=1>
</form></center>
"""

HELP_CONTENT = """
<center><h2>Introduction</h2></center>
<pre class='prebox'>
Temporary file sharing using simple two digit codes (1296 unique). Each file
can have a max number of downloads and password associated with it, defaults
are 1 download and no password. Max upload size of 128M.

You can upload and download from the web. After uploading to the main site, you
will be given a 2 digit code which you may share. A particular code may then be
downloaded by visiting http://thesite.com/&lt;code&gt;.

To upload and download from command line, we recommend using tmpr as well. This
can be done by the short commands u, d (see `tmpr --help` for details).

    pip install tmpr
    tmpr c --url=&lt;some url&gt;       # set the default tmpr url
    tmpr u /path/to/some/file     # upload file and get 2 digit code
    tmpr d &lt;code&gt;                 # download the code

You can also start a server of your own using:

    tmpr s --port=&lt;port&gt;
</pre>

<center><h2>API endpoints</h2></center>
<pre id='endpoints' class='prebox'>
GET
    /               -- this information
    /CODE?ARGS      -- get a file given by CODE

    ARGS (optional) :
        key - password associated with the file
        v - view contents on website rather than raw download

    Examples :
        curl http://URL/ui
        curl http://URL/ab?key=supersecretpassword
        firefox http://URL/7e?v

POST
    /?ARGS          -- upload a file and receive a name
    /CODE?ARGS      -- upload file to a particular name

    ARGS (optional) :
        key -- set a password for a particular file
        n   -- set number of times a file can be downloaded

    Example :
        curl -XPOST -F file=@filename http://URL/?key=secretpassword&n=3

# function for .bashrc to simplify upload
function tmpr() { curl -X POST -F file=@"$$1" &lt;URL&gt; }
</pre>
"""

PAGE_INDEX = INDEX_CONTENT.substitute(favicon=FAVICON, content=FORM_CONTENT)
PAGE_HELP = INDEX_CONTENT.substitute(favicon=FAVICON, content=HELP_CONTENT)

#=============================================================================
# The actual web application now
#=============================================================================
class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r"/help", HelpHandler),
            (r"/([a-z0-9]{2})?", MainHandler)
        ]
        super(Application, self).__init__(handlers, gzip=True)

class HelpHandler(tornado.web.RequestHandler):
    def get(self):
        self.write(PAGE_HELP)
        self.finish()

class MainHandler(tornado.web.RequestHandler):
    def prepare(self, *args, **kwargs):
        self.request.connection.set_max_body_size(int(1e8))
        super(MainHandler, self).prepare(*args, **kwargs)

    def generate_name(self):
        return ''.join([random.choice(CHARS) for i in range(2)])

    def unique_name(self):
        tries, out = 0, self.generate_name()
        while self.exists(out):
            if tries < len(CHARS)**2:
                raise Exception

            out = self.generate_name()
            tries += 1
        return out

    def path(self, n):
        return os.path.join(ROOT, n)

    def pathj(self, n):
        return os.path.join(ROOT, '{}.json'.format(n))

    def save_file(self, name, content, meta):
        with open(self.path(name), 'w') as f:
            f.write(content)

        with open(self.pathj(name), 'w') as f:
            f.write(json.dumps(meta))

    def open_file(self, name):
        data = open(self.path(name)).read()
        meta = json.loads(open(self.pathj(name)).read())
        return data, meta

    def delete_file(self, name):
        os.remove(self.path(name))
        os.remove(self.pathj(name))

    def exists(self, name):
        return os.path.isfile(self.path(name))

    def error(self, text):
        self.clear()
        self.set_status(404)
        self.write(text)
        self.finish()

    def serve_file_headers(self, meta):
        self.set_header('Content-Type', meta['content_type'])
        self.set_header('Content-Disposition', 'attachment; filename='+meta['filename'])

    def serve_file(self, data, meta):
        self.serve_file_headers(meta)
        self.write(data)

    def write_formatted(self, data, meta):
        typ = meta['content_type']

        if 'image' in typ:
            # display images directly in browser
            self.write("<img src='data:%s;base64,%s'/>" % (typ, base64.b64encode(data)))
        elif 'text' in typ:
            # display code and text in pre block
            self.write('<pre>%s</pre>' % data)
        else:
            # otherwise, just download the file like usual
            self.serve_file(data, meta)

    def get(self, args):
        agent = self.request.headers['User-Agent']

        if not args:
            self.write(PAGE_INDEX)
            self.finish()
        else:
            if not self.exists(args):
                self.error('not found')
                return

            data, meta = self.open_file(args)

            # check the key is present if required
            if meta['key']:
                if not meta['key'] == self.request.arguments.get('key', [''])[0]:
                    self.error('key invalid')
                    return

            # either delete the file or update the view count in the meta data
            meta['n'] -= 1
            if meta['n'] == 0:
                self.delete_file(args)
            else:
                self.save_file(args, data, meta)

            # if we are on command line, just return data, otherwise display it pretty
            if 'curl' in agent or 'Wget' in agent:
                self.serve_file(data, meta)
            elif 'v' in self.request.arguments.keys():
                self.write_formatted(data, meta)
            else:
                self.serve_file(data, meta)
            self.finish()

    def post(self, args):
        meta = {}
        meta['key'] = self.request.arguments.get('key', [None])[0]
        usern = int(self.request.arguments.get('n', [1])[0])
        usern = max(min(usern, 10), 0)
        meta['n'] = usern

        # change to error occured since file already exists
        if args and self.exists(args):
            self.error('exists')
            return

        if len(self.request.files) == 1:
            # we have files attached, save each of them to new file names
            name = args or self.unique_name()
            fobj = self.request.files.values()[0][0]

            # separate the actual contents from the meta data
            body = fobj.pop('body')
            meta.update(fobj)

            # write the file and return the accepted name
            self.save_file(name, body, meta)
            self.write(name)
            self.finish()
            return

        self.error('improper payload')

#=============================================================================
# command line utility features
#=============================================================================
def conf_file():
    return os.path.expanduser('~/.tmpr.json')

def argformat(d):
    out = {k:v for k,v in d.items() if v}
    return '?'+urlencode(out) if out else ''

def conf_read(key):
    filename = conf_file()
    if os.path.exists(filename):
        return json.load(open(filename)).get(key)
    return {}

def conf(url='', password=''):
    filename = conf_file()
    if os.path.exists(filename):
        cf = json.load(open(filename))
    else:
        cf = {}

    if url:
        cf.update({'url': url})
    if password:
        cf.update({'pass': password})
    json.dump(cf, open(filename, 'w'))

def download(url, code, password='', browser=False):
    """ Download a file 'code' from the tmpr 'url' """
    url = url or conf_read('url')
    password = password or conf_read('pass')

    if not url:
        print("No URL provided! Provide one or set on via conf.")
        sys.exit(1)

    if browser:
        arg = argformat({'key': password, 'v': 1})
        rqt = '{}{}'.format(urlparse.urljoin(url, code), arg)
        webbrowser.open(rqt, new=True)
        return

    arg = argformat({'key': password})
    rqt = '{}{}'.format(urlparse.urljoin(url, code), arg)
    hdr = {'User-Agent': 'tmpr/{}'.format(__version__)}
    response = requests.get(rqt, headers=hdr)

    # if we get an error, print the error and stop
    if response.status_code != 200:
        print(
            "Code '{}' not found at '{}', '{}'".format(
                code, url, response.content.decode('utf-8')
            ), file=sys.stderr
        )
        sys.exit(1)

    headers = response.headers
    contents = response.content
    response.close()

    # extract the intended filename from the headers
    filename = re.match('.*filename=(.*)$', headers['Content-Disposition']).groups()[0]

    # make sure we are not overwriting any files by appending numbers to the end
    if os.path.exists(filename):
        for i in range(1000):
            newname = '{}-{}'.format(filename, i)
            if not os.path.exists(newname):
                filename = newname
                break

    with open(filename, 'wb') as f:
        f.write(contents)

    print(filename)

def upload(url, filename, code='', password='', num=None):
    """ Upload the file 'filename' to tmpr url """
    url = url or conf_read('url')
    password = password or conf_read('pass')

    if not url:
        print("No URL provided! Provide one or set on via conf.", file=sys.stderr)
        sys.exit(1)

    url = url if not code else urlparse.urljoin(url, code)
    arg = {} if not password else {'key': password}
    arg = arg if num == 1 else dict(arg, n=num)

    name = os.path.basename(filename)

    if not os.path.exists(filename):
        print("File '{}' does not exist".format(filename), file=sys.stderr)
        sys.exit(1)

    with open(filename) as f:
        hdr = {'User-Agent': 'tmpr/{}'.format(__version__)}
        r = requests.post(url, data=arg, files={name: f.read()}, headers=hdr)
        print(r.content.decode('utf-8'))

#=============================================================================
# command line parsing and main
#=============================================================================
class ShortFormatter(argparse.HelpFormatter):
    def _format_action_invocation(self, action):
        if not action.option_strings:
            default = self._get_default_metavar_for_positional(action)
            metavar, = self._metavar_formatter(action, default)(1)
            return metavar

        else:
            parts = []

            # if the Optional doesn't take a value, format is:
            #    -s, --long
            if action.nargs == 0:
                parts.extend(action.option_strings)

            # if the Optional takes a value, format is:
            #    -s ARGS, --long ARGS
            else:
                default = self._get_default_metavar_for_optional(action)
                args_string = self._format_args(action, default)
                for option_string in action.option_strings:
                    parts.append(option_string)

                return '%s %s' % (', '.join(parts), args_string)
            return ', '.join(parts)

    def _get_default_metavar_for_optional(self, action):
        return action.dest.upper()

    def _get_default_metavar_for_positional(self, action):
        return action.dest

    # also raw print the description text
    def _fill_text(self, text, width, indent):
        return ''.join([indent + line for line in text.splitlines(True)])

description = "Simple file sharing utility with download limits and password protection"
epilog = """
Example usage:
    # set the default url, upload a file, and download the file
    tmpr c --url=http://tmpr.meganet.com/
    tmpr u /path/to/file
    tmpr d yu

    # upload a file with a password
    tmpr upload --pass=34lkjsmdfn3i4usldf filename.txt

    # upload to a particular code
    tmpr upload -c 00 filename.txt
"""

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description=description, epilog=epilog,
        formatter_class=ShortFormatter
    )
    sub = parser.add_subparsers()

    # shared arguments between upload and download
    shared = argparse.ArgumentParser(add_help=False)
    shared.add_argument("-u", "--url", type=str, default='',
        help="URL of tmpr service with which to interact")
    shared.add_argument("-p", "--pass", type=str, default='',
        help="password for uploaded file")

    # the sub actions that can be performed
    def _fmt(name):
        if sys.version_info[0] >= 3:
            return {'name': name, 'aliases': [name[0]]}
        return {'name': name[0]}
    kw = {'formatter_class': ShortFormatter}
    kw2 = dict(kw, parents=[shared])

    p_conf = sub.add_parser(help="configure defaults for tmpr", **dict(_fmt('conf'), **kw2))
    p_serve = sub.add_parser(help="run the tmpr webserver", **dict(_fmt('serve'), **kw))
    p_upload = sub.add_parser(help="upload a file to tmpr", **dict(_fmt('upload'), **kw2))
    p_download = sub.add_parser(help="download an uploaded file", **dict(_fmt('download'), **kw2))

    p_conf.set_defaults(action='conf')
    p_serve.set_defaults(action='serve')
    p_download.set_defaults(action='download')
    p_upload.set_defaults(action='upload')

    # custom arguments for server action
    p_serve.add_argument("-a", "--addr", type=str, default='127.0.0.1',
        help="interface / address on which to run the service")
    p_serve.add_argument("-p", "--port", type=int, default=8888,
        help="port on which to run the server")
    p_serve.add_argument("-r", "--root", type=str, default=ROOT,
        help="directory in which to store the uploaded files")

    # custom arguments for upload action
    p_upload.add_argument("-n", "--num", type=int, default=1,
        help="number of downloads available for this file")
    p_upload.add_argument("-c", "--code", type=str,
        help="optional code for uploaded file")
    p_upload.add_argument("filename", type=str, help="name of file to upload")

    # custom arguments for download action
    p_download.add_argument(
        "-b", "--browser", dest='browser', action='store_true',
        help="open the file in a browser"
    )
    p_download.add_argument("code", type=str, help="code of download file")

    # version information
    parser.add_argument('-v', '--version', action='version', version='%(prog)s '+__version__)

    args = vars(parser.parse_args())
    action = args.get('action')

    if action == 'serve':
        ROOT = args.get('root')
        if not os.path.exists(ROOT):
            os.mkdir(ROOT)

        app = Application()
        app.listen(args.get('port'), args.get('addr'))
        tornado.ioloop.IOLoop.instance().start()

    elif action == 'download':
        download(
            args.get('url'), args.get('code'), 
            password=args.get('pass'), browser=args.get('browser')
        )

    elif action == 'upload':
        upload(
            args.get('url'), args.get('filename'),
            code=args.get('code'), num=args.get('num'), password=args.get('pass')
        )

    elif action == 'conf':
        conf(
            url=args.get('url'), password=args.get('pass')
        )

    else:
        parser.print_help()
