# coding: UTF-8

import sys, os, time
import glob, json, argparse, copy
import tempfile
import socket, webbrowser
from wsgiref.simple_server import WSGIRequestHandler, make_server
from bottle import *
from serial_manager import SerialManager
import threading
import urllib
import subprocess
import datetime
import shutil
import hashlib

APPNAME = "FABOOLLaser"
VERSION = "2.7.0"
COMPANY_NAME = "smartdiys"
SERIAL_PORT = None
BITSPERSECOND = 57600
NETWORK_PORT = 4444
TOLERANCE = 0.08
FIRMWARE_FLG = False
CONNECT_THREAD_EXEC_FLG = False


if os.name == 'nt': #sys.platform == 'win32': 
    GUESS_PREFIX = "STMicroelectronics Virtual COM Port"   
elif os.name == 'posix':
    if sys.platform == "linux" or sys.platform == "linux2":
        GUESS_PREFIX = "ttyACM"
    elif sys.platform == "darwin":
        GUESS_PREFIX = "usbmodem"    

else:
    GUESS_PREFIX = "no prefix"    

def storage_dir():
    directory = ""
    if sys.platform == 'darwin':
        # from AppKit import NSSearchPathForDirectoriesInDomains
        # # NSApplicationSupportDirectory = 14
        # # NSUserDomainMask = 1
        # # True for expanding the tilde into a fully qualified path
        # appdata = path.join(NSSearchPathForDirectoriesInDomains(14, 1, True)[0], APPNAME)
        directory = os.path.join(os.path.expanduser('~'), 'Library', 'Application Support', COMPANY_NAME, APPNAME)

    elif sys.platform == 'win32':
        directory = os.path.join(os.path.expandvars('%APPDATA%'), COMPANY_NAME, APPNAME)

    else:
        directory = os.path.join(os.path.expanduser('~'), "." + APPNAME)

    if not os.path.exists(directory):
        os.makedirs(directory)

    return directory

class ConnectThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):

        global SERIAL_PORT

        SerialManager.cancel_queue();

        SERIAL_PORT = SerialManager.match_device(GUESS_PREFIX, BITSPERSECOND)
        if SERIAL_PORT is not None:
            SerialManager.connect(SERIAL_PORT, BITSPERSECOND)

        global CONNECT_THREAD_EXEC_FLG
        CONNECT_THREAD_EXEC_FLG = False


class HackedWSGIRequestHandler(WSGIRequestHandler):
    """ This is a heck to solve super slow request handling
    on the BeagleBone and RaspberryPi. The problem is WSGIRequestHandler
    which does a reverse lookup on every request calling gethostbyaddr.
    For some reason this is super slow when connected to the LAN.
    (adding the IP and name of the requester in the /etc/hosts file
    solves the problem but obviously is not practical)
    """
    def address_string(self):
        """Instead of calling getfqdn -> gethostbyaddr we ignore."""
        # return "(a requester)"
        return str(self.client_address[0])


def run_with_callback(host, port):
    """ Start a wsgiref server instance with control over the main loop.
        This is a function that I derived from the bottle.py run()
    """
    handler = default_app()
    server = make_server(host, port, handler, handler_class=HackedWSGIRequestHandler)
    server.timeout = 0.01
    server.quiet = True

    global FIRMWARE_FLG
    global CONNECT_THREAD_EXEC_FLG

    while 1:
        try:
            if FIRMWARE_FLG == False:
                if SerialManager.is_connected():
                    SerialManager.send_queue_as_ready()
                else:
                    if CONNECT_THREAD_EXEC_FLG == False:
                        CONNECT_THREAD_EXEC_FLG = True
                        thConnect = ConnectThread()
                        thConnect.start()

            server.handle_request()
        except KeyboardInterrupt:
            break
    SerialManager.close()
    # LinuxだけUSBをリセットする
    if os.name == 'posix' and (sys.platform == "linux" or sys.platform == "linux2"):
        try:
            import fcntl

            lsusb_out = subprocess.Popen("lsusb | grep -i STMicroelectronics", shell=True, bufsize=64, stdin=subprocess.PIPE, stdout=subprocess.PIPE, close_fds=True).stdout.read().strip().split()
            bus = lsusb_out[1]
            device = lsusb_out[3][:-1]
            f = open("/dev/bus/usb/%s/%s"%(lsusb_out[1], lsusb_out[3][:-1]), 'w', os.O_WRONLY)
            USBDEVFS_RESET= 21780
            fcntl.ioctl(f, USBDEVFS_RESET, 0)
        except:
            pass


### クロスドメイン対応

@hook('after_request')
def enable_cors():
    response.headers['Access-Control-Allow-Origin'] = '*'
    response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
    response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'


@route('/stash_download', method='POST')
def stash_download():
    """Create a download file event from string."""
    filedata = request.forms.get('filedata')
    fp = tempfile.NamedTemporaryFile(mode='w', delete=False)
    filename = fp.name
    with fp:
        fp.write(filedata)
        fp.close()
    print filedata
    print "file stashed: " + os.path.basename(filename)
    return os.path.basename(filename)

@route('/download/:filename/:dlname')
def download(filename, dlname):
    print "requesting: " + filename
    return static_file(filename, root=tempfile.gettempdir(), download=dlname)

  
@route('/dfu_firmware_download', method='POST')
def dfu_firmware_download():
    # URL取得
    downloadurl = request.forms.get('downloadurl')
    # ファームウェア保存先
    urllist = downloadurl.split("/")
    firmwarepath = tempfile.gettempdir() + os.sep + urllist[len(urllist)-1]

    # ダウンロード
    try:
        urllib.urlretrieve(downloadurl, firmwarepath)
    except IOError:
        return ""

    return firmwarepath


@route('/dfu_firmware_flash', method='POST')
def dfu_firmware_flash():
    global FIRMWARE_FLG
    commandret = '1'

    # ファームウェアのパス取得
    firmwarepath = request.forms.get('firmwarepath')
    # コマンド待ち時間取得
    waittime = request.forms.get('waittime')

    # シリアルポートOpen処理を停止する
    FIRMWARE_FLG = True

    # シリアルポートを閉じる
    if SerialManager.is_connected():
        SerialManager.close()

    # コマンド実行
    if sys.platform == "darwin":  # OSX
        command = os.path.abspath(os.path.dirname(__file__)) + "/dfu/bin/dfu-util -a 0 -D " + firmwarepath
        try:
            flashret = subprocess.call(command, shell=True)
        except OSError:
            commandret = '0'
        if flashret != 0:
            commandret = '0'

    elif sys.platform == "win32": # Windows
        command = os.path.abspath(os.path.dirname(__file__)) + "\\DfuSe\\Bin\\DfuSeCommand.exe -c -d --fn " + firmwarepath
        try:
            process = subprocess.Popen(command, shell=True)
            iCnt = 0
            while process.poll() is None:
                time.sleep(0.1)
                iCnt = iCnt + 1
                if iCnt > waittime:
                    process.kill();
                    commandret = '0'
                    break
        except OSError:
            commandret = '0'

    elif sys.platform == "linux" or sys.platform == "linux2":  #Linux
        # Linuxではファームウェアの更新に対応しない
        commandret = '0'


    # シリアルポートOpen処理を再開する
    FIRMWARE_FLG = False

    return commandret


@route('/version')
def get_version():
    status = copy.deepcopy(SerialManager.get_hardware_status())

    version = {
        'os': sys.platform,
        'firmware_version': status['firmware_version'],
        'grbl_name': status['grbl_name'],
        'app_version': VERSION
    }

    return json.dumps(version)

@route('/serial/:connect')
def serial_handler(connect):
    global FIRMWARE_FLG

    if connect == '1':
        return ""          
    elif connect == '0':
        # print 'js is asking to close serial'    
        if SerialManager.is_connected():
            # シリアルポートOpen処理を停止する
            FIRMWARE_FLG = True
            ret = SerialManager.close()
            # シリアルポートOpen処理を再開する
            FIRMWARE_FLG = False

            if ret: return "1"
            else: return ""  
        return ""
    elif connect == "2":
        # print 'js is asking if serial connected'
        if SerialManager.is_connected(): return "1"
        else: return ""
    else:
        return ""


@route('/status')
def get_status():
    status = copy.deepcopy(SerialManager.get_hardware_status())
    status['serial_connected'] = SerialManager.is_connected()
    status['lasaurapp_version'] = VERSION
    return json.dumps(status)


@route('/pause/:flag')
def set_pause(flag):
    # returns pause status
    if flag == '1':
        if SerialManager.set_pause(True):
            #print "pausing ..."
            return '1'
        else:
            return '0'
    elif flag == '0':
        #print "resuming ..."
        if SerialManager.set_pause(False):
            return '1'
        else:
            return '0'

@route('/gcode', method='POST')
def job_submit_handler():
    job_data = request.forms.get('job_data')
    if job_data and SerialManager.is_connected():
        lines = job_data.split('\n')
        #print "Adding to queue %s lines" % len(lines)
        for line in lines:
            SerialManager.queue_gcode_line(line)
        return "__ok__"
    else:
        return "serial disconnected"

@route('/queue_pct_done')
def queue_pct_done_handler():
    return SerialManager.get_queue_percentage_done()


### オフライン用API
@route('/')
def default_handler():
    return static_file('app.html', root=get_frontend_dir())

@route('/p/:hash#[A-Z0-9]+#')
def hash_handler(hash):
    return static_file('app.html', root=get_frontend_dir())

@route('/:path#.+\.(css|js|map|eot|woff|woff2|svg|jpg|png|gif)#')
def static_handler(path):
    return static_file(path, root=get_frontend_dir())

@route('/api/v1/projects/')
def get_projects():
    id = None
    id_hash = None
    try:
        id = request.GET.dict['id'][0]
    except:
        pass
    try:
        id_hash = request.GET.dict['id_hash'][0]
    except:
        pass

    if id != None:
        return get_project_id(id)
    elif id_hash != None:
        return get_project_id_hash(id_hash)
    else:
        return get_projects_list()

def get_projects_list():
    # プロジェクト一覧取得
    files = get_project_list()

    data = []
    for filename in files:
        infoData = get_project_info(filename)
        if infoData != None:
            data_one = {
                        'user':{  
                            'id':1
                        },
                        'published_at':None,
                        'published':False,
                        'name':infoData['name'],
                        'id_hash':filename,
                        'id':infoData['id'],
                        'deleted_at':None,
                        'deleted':False
                        }
            data.append(data_one)

    return json.dumps({'data':data})

def get_project_id(id):
    # idで指定したプロジェクト取得
    id_hash = search_project_id_hash(id)
    if id_hash == "":
        return ''

    return get_project_id_hash(id_hash)

def get_project_id_hash(id_hash):
    # id_hashで指定したプロジェクト取得
    infoData = get_project_info(id_hash)
    if infoData == None:
        return ''
    if infoData.has_key('blue_print'):
        sourceString = get_project_source(id_hash, infoData['blue_print']['id_hash'])
        data = {
                    'user':{  
                        'id':1
                    },
                    'published_at':None,
                    'published':False,
                    'name':infoData['name'],
                    'id_hash':id_hash,
                    'id':infoData['id'],
                    'deleted_at':None,
                    'deleted':False,
                    'blue_print':{  
                        'source':sourceString,
                        'id_hash':infoData['blue_print']['id_hash'],
                        'id':infoData['blue_print']['id']
                    }
                }
    else:
        data = {
                    'user':{  
                        'id':1
                    },
                    'published_at':None,
                    'published':False,
                    'name':infoData['name'],
                    'id_hash':id_hash,
                    'id':infoData['id'],
                    'deleted_at':None,
                    'deleted':False,
                }

    return json.dumps({'data':data})

@route('/api/v1/projects/', method='POST')
def make_project():
    # プロジェクト作成
    req_json = json.load(request.body)
    # ID
    id = get_max_id() + 1
    # ハッシュ
    id_hash = hashlib.sha256(str(1) + str(id)).hexdigest().upper()
    # ディレクトリ作成
    filePath = os.path.join(storage_dir(), id_hash)
    os.makedirs(filePath)
    # info保存
    infoData = {
            'name':req_json['project']['name'],
            'id':id
    }
    set_project_info(id_hash, infoData)
    # Response
    data = {
                'user':{  
                    'id':1
                },
                'published_at':None,
                'published':False,
                'name':infoData['name'],
                'id_hash':id_hash,
                'id':infoData['id'],
                'deleted_at':None,
                'deleted':False,
            }

    return json.dumps({'data':data})

@route('/api/v1/projects/:id', method='PATCH')
def change_project_name(id):
    # プロジェクトの名称変更
    req_json = json.load(request.body)    
    id_hash = search_project_id_hash(id)

    infoData = get_project_info(id_hash)
    infoData['name'] = req_json['project']['name']

    # infoに書き込み
    set_project_info(id_hash, infoData)
    # Response
    data = {
            'user':{  
                'id':1
            },
            'published_at':None,
            'published':False,
            'name':infoData['name'],
            'id_hash':id_hash,
            'id':infoData['id'],
            'deleted_at':None,
            'deleted':False
            }

    return json.dumps({'data':data})

@route('/api/v1/projects/:id', method='DELETE')
def delete_project(id):
    # プロジェクト削除
    id_hash = search_project_id_hash(id)
    infoData = get_project_info(id_hash)
    # 削除実行
    filePath = os.path.join(storage_dir(), id_hash)
    shutil.rmtree(filePath)
    # Response
    data = {
            'user':{  
                'id':1
            },
            'published_at':None,
            'published':False,
            'name':infoData['name'],
            'id_hash':id_hash,
            'id':infoData['id'],
            'deleted_at':datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
            'deleted':True
            }

    return json.dumps({'data':data})

@route('/api/v1/projects/:id_hash/blue_prints/current')
def get_blue_print(id_hash):
    # 最新のblue_print取得
    infoData = get_project_info(id_hash)
    if infoData == None:
        return ''

    sourceString = get_project_source(id_hash, infoData['blue_print']['id_hash'])
    # Response
    data = {
            'project':{  
                'id_hash':id_hash,
                'id':infoData['id']
            },
            'source':sourceString,
            'id_hash':infoData['blue_print']['id_hash'],
            'id':infoData['blue_print']['id']
            }

    return json.dumps({'data':data})

@route('/api/v1/projects/:id_hash/blue_prints', method='POST')
def set_blue_print(id_hash):
    # blue_print作成
    req_json = json.load(request.body)
    infoData = get_project_info(id_hash)

    # info更新
    bp_id = 1
    if infoData.has_key('blue_print'):
        bp_id = infoData['blue_print']['id'] + 1

    infoData = {
        'name':infoData['name'],
        'id':infoData['id'],
        'blue_print':{
            'id':bp_id,
            'id_hash':hashlib.sha256(str(1) + str(infoData['id']) + str(bp_id)).hexdigest().upper()
        }

    }
    set_project_info(id_hash, infoData)

    # blue_printファイル作成
    set_project_source(id_hash, infoData['blue_print']['id_hash'], req_json['blue_print']['source'])

    # Response
    data = {
            'project':{  
                'id_hash':id_hash,
                'id':infoData['id']
            },
            'source':req_json['blue_print']['source'],
            'id_hash':infoData['blue_print']['id_hash'],
            'id':infoData['blue_print']['id']
            }

    return json.dumps({'data':data})


# プロジェクトリスト取得
def get_project_list():
    files = []
    cwd_temp = os.getcwd()
    try:
        os.chdir(storage_dir())
        files = filter(os.path.isdir, glob.glob("*"))
        files.sort(key=lambda x: os.path.getmtime(x))
    finally:
        os.chdir(cwd_temp)

    return files

# 指定プロジェクトのinfo取得
def get_project_info(id_hash):
    infoData = None
    filePath = os.path.join(storage_dir(), id_hash)
    fileInfoPath = os.path.join(filePath, 'info.json')
    if not os.path.exists(fileInfoPath):
        return None
    try:
        fp = open(fileInfoPath, 'r')
        infoData = json.load(fp)
    finally:
        fp.close()

    return infoData

# 指定プロジェクトのinfo設定
def set_project_info(id_hash, infoData):
    filePath = os.path.join(storage_dir(), id_hash)
    fileInfoPath = os.path.join(filePath, 'info.json')

    try:
        fp = open(fileInfoPath, 'w')
        json.dump(infoData, fp)
    finally:
        fp.close()

# idからid_hashを取得
def search_project_id_hash(id):

    files = get_project_list()

    for filename in files:
        infoData = get_project_info(filename)
        if infoData != None and infoData['id'] == int(id):
            return filename

    return ""    

# idの最大値取得
def get_max_id():
    max_id = 0

    files = get_project_list()

    for filename in files:
        infoData = get_project_info(filename)
        if infoData['id'] > max_id:
            max_id = infoData['id']

    return max_id

# 指定プロジェクトの最新のblue_print取得
def get_project_source(id_hash, bp_id_hash):
    sourceString = ''

    filePath = os.path.join(storage_dir(), id_hash)
    fileSourcePath = os.path.join(filePath, bp_id_hash +'.json')
    try:
        fp = open(fileSourcePath, 'r')
        sourceString = fp.read().decode('utf-8')
    finally:
        fp.close()

    return sourceString

# 指定プロジェクトにblue_print設定
def set_project_source(id_hash, bp_id_hash, sourceString):
    filePath = os.path.join(storage_dir(), id_hash)
    fileSourcePath = os.path.join(filePath, bp_id_hash +'.json')
    try:
        fp = open(fileSourcePath, 'w')
        fp.write(sourceString.encode('utf-8'))
    finally:
        fp.close()

# フロントエンドアップロード
@route('/update_frontend', method='POST')
def update_frontend():
    # URL取得
    req_json = json.load(request.body)

    frontenddir = get_frontend_dir()
    if not os.path.exists(frontenddir):
        os.makedirs(frontenddir)

    for downloadurl in req_json['downloadurl']:
        # ファームウェア保存先
        urllist = downloadurl.split("/")
        filepath = frontenddir + os.sep + urllist[len(urllist)-1]

        # ダウンロード
        try:
            urllib.urlretrieve(downloadurl, filepath)
        except IOError:
            return ""

    return "0"

# フロントエンドディレクトリ取得
def get_frontend_dir():
    return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), './frontend'))


### Setup Argument Parser
argparser = argparse.ArgumentParser(description='Run FABOOLLaser.', prog='faboollaser')
argparser.add_argument('-p', '--public', dest='host_on_all_interfaces', action='store_true',
                    default=False, help='bind to all network devices (default: bind to 127.0.0.1)')
argparser.add_argument('-d', '--debug', dest='debug', action='store_true',
                    default=False, help='print more verbose for debugging')
args = argparser.parse_args()

if not args.debug:
    # 標準出力、標準エラーの出力を無効
    sys.stdout = open(os.devnull, "w")
    sys.stderr = open(os.devnull, "w")
    # デバッグ出力を無効
    debug(False)

if args.host_on_all_interfaces:
    run_with_callback('', NETWORK_PORT)
else:
    run_with_callback('127.0.0.1', NETWORK_PORT)    

