#!/usr/bin/env python
"""
recat

Copyright 2015, Doat Media LTD.
Licensed under the Apache License, Version 2.0

logcat-color

Copyright 2012, Marshall Culpepper
Licensed under the Apache License, Version 2.0

Portions Copyright 2009, The Android Open Source Project
Thanks to Jeff Sharkey, the author of coloredlogcat.py,
the original inspiration of logcat-color
"""
import asyncore
import errno
import fcntl
import optparse
import os
import struct
import sys
import termios

from colorama import Fore, Back, Style
from subprocess import check_call, Popen, PIPE
from logcatcolor.config import LogcatColorConfig
from logcatcolor.mapping import MappingFetcher
from logcatcolor.profile import Profile
from logcatcolor.reader import LogcatReader
from logcatcolor.retrace import Retracer


class LogcatColor(object):
    def __init__(self, args=None):
        self.parse_args(args)
        self.width = self.get_term_width()
        self.config = LogcatColorConfig(self.options)

        self.profile = None
        if len(self.args) >= 1:
            self.profile = Profile.get_profile(self.args[0])
            if self.profile:
                self.args = self.args[1:]

        if not self.profile:
            self.logcat_args.extend(self.args)

        self.format = None
        if self.options.format:
            self.format = self.options.format
        elif self.profile and self.profile.format:
            self.format = self.profile.format

        self.layout = self.format
        if self.options.plain:
            self.layout = "raw"

    def get_term_width(self):
        out_fd = self.output.fileno()
        if os.isatty(out_fd):
            # unpack the current terminal width / height
            data = fcntl.ioctl(out_fd, termios.TIOCGWINSZ, '1234')
            height, width = struct.unpack('hh', data)
        else:
           # store a large width when the output of this script is being piped
           width = 2000

        return width

    def parse_args(self, args=None):
        parser = optparse.OptionParser()

        # logcat-color options
        parser.add_option("--config", dest="config", default=None,
            help="path to logcat-color config file (default: ~/.logcat-config)")
        parser.add_option("--plain", action="store_true", dest="plain", default=False,
            help="apply profiles and filters, but don't colorize / format output (useful for logging to a file)")
        parser.add_option("--no-wrap", action="store_false", dest="wrap", default=None,
            help="don't wrap console text into a column (makes for better copy/paste)")
        parser.add_option("--stay-connected", action="store_true", default=None, dest="stay_connected",
            help="keep logcat-color running when the device disconnects, and automatically wait for the device to reconnect")
        parser.add_option("-i", "--input", metavar="FILE", dest="input", default=None,
            help="read input from FILE, instead of starting adb. this is equivalent to piping FILE to logcat-color. (default: start adb, and read from it's stdout)")
        parser.add_option("-o", "--output", metavar="FILE", dest="output", default=None,
            help="write output to FILE (default: stdout)")
        parser.add_option("-m", "--mapping", dest="mapping", default=None,
            help="On-the-fly deobfuscation using retrace (need to supply a mapping file)")
        parser.add_option("-t", "--tag-prefixes", dest="tag_prefixes", default=[],
            help="prefixes of tags which ought to be deobfuscated")

        # ADB options
        parser.add_option("-d", "--device", action="store_const", dest="adb_device", const="device",
            help="connect to the only plugged-in device")
        parser.add_option("-e", "--emulator", action="store_const", dest="adb_device", const="emulator",
            help="connect to the only running emulator")
        parser.add_option("-s", "--serial-number", dest="adb_device",
            help="connect to a specific device by it's serial number")

        # Logcat options
        # See http://developer.android.com/guide/developing/tools/logcat.html
        # We can't support -d / -s since we use them for ADB above, but we
        # provide long-form options in case they are needed
        parser.add_option("-b", "--buffer", action="append", dest="buffers",
            help="loads an alternate log buffer for viewing, such as event or radio")
        parser.add_option("-c", "--clear", action="append_const", dest="logcat_args", const="-c",
            help="clears (flushes) the entire log and exits")
        parser.add_option("--dump", action="append_const", dest="logcat_args", const="-d",
            help="dumps the log to the screen and exits")
        parser.add_option("-f", "--file", dest="file", default=None,
            help="writes log message output to <file>. the default is stdout")
        parser.add_option("-g", "--print-size", action="append_const", dest="logcat_args", const="-g",
            help="prints the size of the specified log buffer and exits")
        parser.add_option("-n", "--max-rotated-logs", dest="max_rotated_logs", type="int",
            help="sets the maximum number of rotated logs. requires the -r option (default: 4)")
        parser.add_option("-r", "--rotate", dest="rotate_kbytes", type="int",
            help="rotates the log file every <rotate_kbytes> of output. requires the -f option (default: 16)")
        parser.add_option("--silent", action="append_const", dest="logcat_args", const="-s",
            help="sets the default filter spec to silent")
        parser.add_option("-v", "--format", dest="format", default=None,
            help="sets the output format for log messages. possible formats: brief, process, tag, raw, time, threadtime, long (default: brief)")

        (options, args) = parser.parse_args(args)
        self.options = options
        self.args = args

        if options.config and not os.path.isfile(options.config):
            parser.error("Config file does not exist: %s" % options.config)

        self.input = sys.stdin
        if options.input:
            self.input = open(options.input, "r")

        self.output = sys.stdout
        if options.output:
            self.output = open(options.output, "w")

        self.adb_device = options.adb_device
        self.logcat_args = options.logcat_args or []
        self.mapping = options.mapping
        self.tag_prefixes = options.tag_prefixes

        if options.buffers:
            for buf in options.buffers:
                self.logcat_args.extend(["-b", buf])

        if options.file:
            self.logcat_args.extend(["-f", options.file])

        if options.max_rotated_logs:
            self.logcat_args.extend(["-n", options.max_rotated_logs])

        if options.rotate_kbytes:
            self.logcat_args.extend(["-r", options.rotate_kbytes])

    def get_adb_args(self):
        adb = "adb" # Let the system find adb on the PATH
        if "ADB" in os.environ:
            adb = os.environ["ADB"]

        config_adb = self.config.get_adb()
        if config_adb:
            adb = config_adb

        adb_args = [adb]
        if not self.adb_device and self.profile:
            emulator = self.profile.emulator
            if emulator:
                self.adb_device = \
                    emulator if type(emulator) is str else "emulator"

            device = self.profile.device
            if device:
                self.adb_device = device if type(device) is str else "device"

        if self.adb_device == "emulator":
            adb_args.append("-e")
        elif self.adb_device == "device":
            adb_args.append("-d")
        elif self.adb_device:
            adb_args.extend(["-s", self.adb_device])

        return adb_args

    def get_logcat_args(self):
        logcat_args = self.logcat_args[:]
        if self.format:
            # put format in front in case custom filters are used
            logcat_args[0:0] = ["-v", self.format]

        if self.profile:
            buffers = self.profile.buffers
            if buffers:
                for b in buffers: logcat_args.extend(["-b", b])

        return logcat_args

    def start_logcat(self):
        adb_command = self.get_adb_args()
        adb_command.append("logcat")
        adb_command.extend(self.get_logcat_args())
        try:
           self.input = Popen(adb_command, stdin=PIPE, stdout=PIPE, stderr=PIPE).stdout
        except OSError, e:
            if e.errno == errno.ENOENT:
                print >>sys.stderr, \
                    'Error, adb could not be found using: "%s"\n' \
                    'To fix this: \n' \
                    '  1) Add the directory containing adb to your PATH\n' \
                    '  2) Set the ADB environment variable\n' \
                    '  3) Set "adb" in ~/.logcat-color' % adb_command[0]
            else:
                print >>sys.stderr, 'Could not run ADB: %s' % str(e)
            sys.exit(e.errno)

    def init_reader(self):

        source = None
        mapping_fetcher = MappingFetcher('', self.mapping)
        mapping_file = mapping_fetcher.get_mapping_file()

        retracer = None
        if mapping_file:
            retracer = Retracer(mapping_file, self.tag_prefixes)

        # If not obfuscated or failed fetching obfuscation file, fail back to original input
        if not source:
            source = self.input

        reader = LogcatReader(self.input, self.config, profile=self.profile,
            format=self.format, layout=self.layout, writer=self.output,
            width=self.width, tag_prefixes=self.tag_prefixes, retracer=retracer)

    def start(self):
        # if someone is piping, use stdin as input. if not, invoke adb logcat
        # if self.input.isatty():
        self.start_logcat()

        self.init_reader()

    def loop(self):
        try:
            self.start()
            while True:
                asyncore.loop()
                if not self.config.get_stay_connected():
                    break
                self.wait_for_device()
                self.start_logcat()
                self.init_reader()
        except KeyboardInterrupt, e:
            pass

    WAIT_FOR_DEVICE = Fore.WHITE + Back.BLACK + Style.DIM + \
                      "--- Waiting for device" + Style.RESET_ALL + \
                      Fore.BLUE + Back.BLACK + Style.DIM + " %s" +  Style.RESET_ALL + \
                      Fore.WHITE + Back.BLACK + Style.DIM + "---" + Style.RESET_ALL

    def wait_for_device(self):
        command = self.get_adb_args()
        command.append("wait-for-device")

        device_str = ""
        if self.adb_device:
            device_str = "\"%s\" " % self.adb_device

        print self.WAIT_FOR_DEVICE % device_str
        check_call(command)

if __name__ == "__main__":
    LogcatColor().loop()
