import sys
import datetime
import subprocess
import os
from time import sleep

import serial
import pexpect
import pexpect.exceptions
import pexpect.fdpexpect

import logging

LATEST_VERSION = "4.3.3.0"
LATEST_REVISION = "35577"
FIRMWARE_FILENAME_SUFFIX = ".upgrade.usf"
LOGGING_DIR = "/var/log/"
GLIN_LOG_FILE = "glin_log.txt"

upgrade_attempts = 0
MAX_ATTEMPTS = 2

class LteTimeoutError(Exception):
    pass


class LteSerialError(Exception):
    pass


class LteUpgradeError(Exception):
    pass


def get_fw_version():
    """Asks the modem for its firmware version information.

    Returns:
        (tuple): Tuple containing:
            version(string): Version string, like "4.3.3.0".
            revision(string): Revision number, like 35577.

    Raises:
        LteSerialError: If the serial connection to the modem fails.
        LteTimeoutError: If the version information request times out.
    """
    ser = serial.Serial()
    ser.baudrate = 115200
    ser.port = "/dev/ttyACM0"
    try:
        ser.open()
    except OSError as e:
        if e.errno == 13:
            raise LteSerialError("Permission denied. Try running with 'sudo'.")
        raise LteSerialError("Unable to open serial connection to the LTE modem. Is it already in use? ({e})".format(e=e.errno))

    ser.write('ATI1\n')
    temp = pexpect.fdpexpect.fdspawn(ser, timeout=3)
    try:
        temp.expect("A-REVISION ([0-9.]+)-([0-9]+)\r\n")
    except pexpect.exceptions.TIMEOUT:
        raise LteTimeoutError("Did not receive response containing updated LTE firmware version.")
    finally:
        temp.close()
    return (temp.match.group(1), temp.match.group(2))


def upgrade_fw(port="/dev/ttyACM0", firmware_file=LATEST_REVISION+FIRMWARE_FILENAME_SUFFIX):
    """Attempts to upgrade the cell modem's firmware.

    Args:
        port (string): Serial port of the cell modem.
        firmware_file (string): Firmware file to upload.

    Raises:
        LteUpgradeError: If the upgrade process fails.
    """

    command = os.path.join(os.path.dirname(os.path.abspath(__file__)), "glinswup_APLx_static")
    with open(os.path.join(LOGGING_DIR, GLIN_LOG_FILE), 'w') as glin_log:
        rc = subprocess.call([command, "-p", port, "-f", firmware_file], stdout=glin_log)

    if rc != 0:
	    raise LteUpgradeError("Upgrade process failed with return code '{rc}'".format(rc=rc))


def modem_needs_update(version, revision):
    """Determines if the given version info indicates that an upgrade is needed.

    Args:
        version (string): Version string, like "4.3.3.0".
        revision (string): Revision number, like 35577.

    Returns:
        int | None: Firmware revision needed to upgrade further. None if firmware is up-to-date.
    """
    if int(revision) < 29979:
        return "29979"  # We need to upgrade to 29979 before going further.
    elif version != LATEST_VERSION or revision != LATEST_REVISION:
        return LATEST_REVISION


def wait_for_modem(seconds=150):
    """Attempts to retrieve the modem's firmware info until successful or timeout.

    Args:
        seconds (int): Number of seconds to wait before timing out.

    Returns:
        (tuple): Tuple containing:
            version(string): Version string, like "4.3.3.0".
            revision(string): Revision number, like 35577.

    Raises:
        LteTimeoutError: If timeout is reached with no response.
    """
    starttime = datetime.datetime.now()
    while (datetime.datetime.now() - starttime).seconds < seconds:
        try:
            version_tuple = get_fw_version()
            return version_tuple
        except pexpect.exceptions.EOF:
            pass  # Modem isn't back yet
        except LteSerialError:
            pass  # Serial connection in use, give it a few seconds
        except Exception:
            pass  # Some other unexpected error, let's just keep trying.
        sleep(5)
    raise LteTimeoutError("Timed out waiting for modem.")  # We never heard back from the modem


def run_check():
    """Executes the 'check' CLI command to determine version status."""
    # First, retrieve the firmware version:
    try:
    	(v, r) = get_fw_version()
    except (LteTimeoutError, LteSerialError) as e:
        logging.debug(e)
        return 1

    # Check if an upgrade is needed:
    revision_needed = modem_needs_update(v, r)
    if revision_needed is not None:
        logging.debug("Modem is on version {v}, revision {r}. It is recommended that you run 'python lteupgrade.py upgrade' to upgrade to revision {rnew}.".format(
            v=v, r=r, rnew=revision_needed))
        return 0
    else:
        logging.debug("Modem is already up-to-date, no upgrade needed. ({v}-{r})".format(v=v, r=r))

    return 0


def run_upgrade(force=False):
    """Executes the 'upgrade' or 'force' CLI command to upgrade the modem firmware.

    Args:
        force (bool): If True, force an upgrade, even if already at the latest firmware.
    """
    global upgrade_attempts
    # First, retrieve the firmware version:
    try:
        (v, r) = get_fw_version()
    except (LteTimeoutError, LteSerialError) as e:
        logging.debug(e)
        return 1
    logging.debug("Before upgrading, modem is on version {v}, revision {r}".format(v=v, r=r))

    # Check if an upgrade is needed:
    revision_needed = modem_needs_update(v, r)
    if revision_needed is None:
        if not force:
            logging.debug("Modem is already up-to-date. Run 'python lteupgrade.py force' to upgrade anyway.")
            return 0
        else:
            logging.debug("Modem is already up-to-date. Forcing upgrade anyway.".format())
            revision_needed = LATEST_REVISION

    # Attempt to upgrade the firmware:
    revision_matches = False
    upgrade_attempts = 0
    while (upgrade_attempts < MAX_ATTEMPTS and not revision_matches):
        logging.debug("Upgrading to revision {rev}.".format(rev=revision_needed))
        upgrade_attempts += 1
        try:
            firmware_dir = os.path.dirname(os.path.abspath(__file__))
            firmware_path = os.path.join(firmware_dir, revision_needed + FIRMWARE_FILENAME_SUFFIX)
            upgrade_fw(firmware_file=firmware_path)
        except LteUpgradeError:
            logging.debug("Upgrade failed. Please try again.")
            return 2

        # Verify the upgrade was successful by checking the version information on the modem:
        logging.debug("Verifying new firmware version. This may take a few minutes.")
        try:
            (v, r) = wait_for_modem()
        except LteTimeoutError:
            logging.debug("Did not get a response from modem after upgrade.  Try running 'python lteupgrade.py check' to verify the upgrade was successful.")
            return 3

        logging.debug("After upgrading, modem is on version {v}, revision {r}".format(v=v, r=r))
        if revision_needed != r:
            logging.debug("Post-upgrade revision number ({actual}) did not match expected revision number ({expected}). Trying again.".format(expected=revision_needed, actual=r))
        else:
            logging.debug("Post-upgrade revision number matches expected revision number ({expected}).".format(expected=revision_needed))
            revision_matches = True

    if not revision_matches:
        logging.debug("Exhausted all {x} attempts to upgrade to {expected}. Please try the upgrade again later.".format(x=MAX_ATTEMPTS, expected=revision_needed))
        return 4

    if not force:
        logging.debug("Checking for further updates.")
        run_upgrade()

    return 0


if __name__ == "__main__":
    logging.basicConfig(filename=os.path.join(LOGGING_DIR,'lte-upgrade-log.txt'),level=logging.DEBUG)
    logging.getLogger().addHandler(logging.StreamHandler())

    if len(sys.argv) == 1:
        logging.debug("A command is required. Possible commands are 'check', 'upgrade', and 'force'")
        sys.exit(4)

    command = sys.argv[1].lower()

    rc = 0
    if command == "check":
        rc = run_check()
    elif command == "upgrade":
        rc = run_upgrade()
    elif command == "force":
        rc = run_upgrade(force=True)
    else:
        logging.debug("Invalid command. valid options are 'check', 'upgrade', and 'force'")
        rc = 4

    sys.exit(rc)
