import serial
import logging
import re
import telnetlib
import os
import subprocess
import tarfile
import base64
import string
import random
from time import sleep
from serial.serialutil import Timeout
import StringIO
import tempfile

logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)

class Rooter(object):
    def __init__(self, **params):
        if type(params['port']) is str:
            params['port']=serial.Serial(
                port=params['port'],
                baudrate=115200
            )
        self._port = params['port']
        self._ssh_pubkey_data = params['ssh_pubkey_data']
        self._has_jtag = params['has_jtag']
        self._check_uboot = params['check_uboot']
        self._cleanup_payload = params['cleanup_payload']
        self._reboot_after = params['reboot_after']
        self._uboot_only = params['uboot_only']
        self._boot_only = params['boot_only']
        self._jtag_hardware = params['jtag_hardware']

    def run(self):
        #TODO: clean this up
        port = self._port
        ssh_pubkey_data = self._ssh_pubkey_data
        has_jtag = self._has_jtag
        check_uboot = self._check_uboot
        cleanup_payload = self._cleanup_payload
        reboot_after = self._reboot_after
        uboot_only = self._uboot_only
        boot_only = self._boot_only

        if check_uboot:
            uboot_passwords={
                "2010.09-R6" : "f4E9J",
                "2010.09-R8" : "3BHf2",
                "2010.09"    : "toon"
            }
            log.info("Waiting for Toon to restart")
            uboot_version=self.read_uboot_version()
            log.info("Toon has U-Boot version {}".format(uboot_version))
            if uboot_version in uboot_passwords:
                log.info("Using password to log in")
                self.access_uboot(uboot_passwords[uboot_version])
                if uboot_only:
                    log.info("Your Toon is now waiting in u-boot. You can you use a serial console for manual control.")
                else:
                    self.patch_uboot()
                    if boot_only:
                        log.info("Your Toon is now booting into a serial console")
                    else:
                        log.info("Waiting for boot up")
                        self.write_payload()
                        self.patch_toon()
                        log.info("Your Toon is now rooted. Please wait for it to boot up and try to log in using SSH")
                return
            elif has_jtag is False:
                log.error("Unable to log in using password (need JTAG, but it's disabled)")
                return
        if has_jtag:
            log.info("Loading new bootloader")
            self.start_bootloader("assets/u-boot.bin")
            port.reset_input_buffer()
            self._has_jtag = False
            self._check_uboot = True
            self.run()
        else:
            log.error("Need JTAG when rooting without manual reset")
            return

    def read_uboot_version(self):
        version_line_match = re.compile(r'^U-Boot ([^ ]+)')
        while True:
            line = self._port.readline().strip()
            match = version_line_match.match(line)
            if match:
                return match.group(1)

    def access_uboot(self, password):
        log.info("Logging in to U-Boot")
        self._port.write(password)
        self._port.flush()
        log.debug(self._port.read_until("U-Boot>"))
        log.debug("Logged in to U-Boot")

    def patch_uboot(self):
        port = self._port

        log.info("Patching U-Boot")
        port.reset_input_buffer()
        sleep(0.1)
        port.write("printenv\n")
        port.flush()
        add_misc_match = re.compile(r'^addmisc=(.+)$')
        add_misc_val = None

        sleep(0.5)

        lines = port.read_until("U-Boot>")
        log.debug(lines)
        for line in lines.split('\n'):
            line = line.strip()
            log.debug(line)
            match = add_misc_match.match(line)
            if match:
                add_misc_val = match.group(1)

        if add_misc_val is None:
            log.error("Could not find value for addmisc environment variable")
            return

        cmd = "setenv addmisc " + re.sub(r'([\$;])',r'\\\1', add_misc_val + " init=/bin/sh")
        port.write(cmd + "\n")
        port.flush()
        log.debug(port.read_until("U-Boot>"))
        port.write("run boot_nand\n")
        port.flush()

    def create_payload_tar(self):
        tar_path = tempfile.mkstemp(suffix=".tar.gz")[1]
        ssh_key = self._ssh_pubkey_data
        with tarfile.open(tar_path, "w:gz") as tar:
            tar.add('payload/', arcname='payload')

            ssh_key_str = StringIO.StringIO(ssh_key)

            info = tarfile.TarInfo(name="payload/id_rsa.pub")
            info.size=len(ssh_key)

            tar.addfile(tarinfo=info, fileobj=StringIO.StringIO(ssh_key))
        return tar_path

    def write_payload(self):
        port = self._port
        tar_path = self.create_payload_tar()

        log.debug(port.read_until("/ # "))
        port.write("timeout -t 60 base64 -d | tar zxf -\n")
        port.flush()

        log.info("Transferring payload")
        with open(tar_path, 'r') as f:
            base64.encode(f, port)

        log.info("Transferring payload done. Waiting for toon to finish.")
        os.remove(tar_path)

        port.flush()
        port.reset_input_buffer()
        port.write("\x04")
        port.flush()
        port.write("\n")
        log.debug(port.read_until("/ # "))
        log.info("Transferring payload finished")

    def patch_toon(self):
        (port, clean_up, reboot) = (
            self._port, self._cleanup_payload, self._reboot_after)
        log.info("Patching Toon")
        password = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8))
        port.write("if [ -f payload/patch_toon.sh ] ; then sh payload/patch_toon.sh \"{}\" ; fi\n".format(password))
        try:
            while True:
                line = read_until(port, ["/ # ", "\n"])
                if line == "/ # ":
                    break
                if line.startswith(">>>"):
                    log.info(line.strip())
                else:
                    log.debug(line.strip())
        except:
            log.exception("Script failed")
            sleep(5)
        if clean_up:
            log.info("Cleaning up")
            port.write("rm -r payload\n")
            log.debug(port.read_until("/ # "))
        if reboot:
            log.info("Rebooting")
            port.write("/etc/init.d/reboot\n")

    def start_bootloader(self, bin_path):

        log.info("Starting openocd")

        proc = subprocess.Popen([
            'openocd',
                '-s', '/usr/share/openocd',
                '-f', 'assets/adapters/{}.cfg'.format(self._jtag_hardware),
                '-f', 'assets/boards/ed20.cfg'
            ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        try:
            wait = 10
            log.info("Waiting for {} seconds".format(wait))
            sleep(wait)
            client = telnetlib.Telnet('localhost', 4444)
            log.debug(client.read_until("> "))
            log.info("Halting CPU")
            client.write("soft_reset_halt\n")
            log.debug(client.read_until("> "))
            sleep(0.1)
            client.write("reset halt\n")
            log.debug(client.read_until("> "))
            sleep(0.1)
            log.info("Loading new image to RAM")
            client.write("load_image {} 0xa1f00000\n".format(bin_path))
            log.debug(client.read_until("> "))
            sleep(0.1)
            log.info("Starting up new image")
            client.write("resume 0xa1f00000\n")
        except:
            try:
                log.exception(proc.communicate()[0])
            except:
                pass
            proc.terminate()
            raise

        proc.terminate()

def read_until(port, terminators=None, size=None):
    """\
    Read until any of the termination sequences is found ('\n' by default), the size
    is exceeded or until timeout occurs.
    """
    if not terminators:
        terminators = ['\n']
    terms = map(lambda t: (t, len(t)), terminators)
    line = bytearray()
    timeout = Timeout(port._timeout)
    while True:
        c = port.read(1)
        if c:
            line += c
            for (terminator, lenterm) in terms:
                if line[-lenterm:] == terminator:
                    # break does not work here because it will only step out of for
                    return bytes(line)
            if size is not None and len(line) >= size:
                break
        else:
            break
        if timeout.expired():
            break
    return bytes(line)
