# Module to process general user input options (scan length, starting
# offset, etc).

import io
import os
import re
import sys
import argparse
import binwalk.core.idb
import binwalk.core.common
import binwalk.core.display
import binwalk.core.settings
from binwalk.core.module import Module, Option, Kwarg, show_help


class General(Module):

    TITLE = "General"
    ORDER = 0

    DEFAULT_DEPENDS = []

    CLI = [
        Option(long='length',
               short='l',
               type=int,
               kwargs={'length': 0},
               description='Number of bytes to scan'),
        Option(long='offset',
               short='o',
               type=int,
               kwargs={'offset': 0},
               description='Start scan at this file offset'),
        Option(long='base',
               short='O',
               type=int,
               kwargs={'base': 0},
               description='Add a base address to all printed offsets'),
        Option(long='block',
               short='K',
               type=int,
               kwargs={'block': 0},
               description='Set file block size'),
        Option(long='swap',
               short='g',
               type=int,
               kwargs={'swap_size': 0},
               description='Reverse every n bytes before scanning'),
        Option(long='log',
               short='f',
               type=argparse.FileType,
               kwargs={'log_file': None},
               description='Log results to file'),
        Option(long='csv',
               short='c',
               kwargs={'csv': True},
               description='Log results to file in CSV format'),
        Option(long='term',
               short='t',
               kwargs={'format_to_terminal': True},
               description='Format output to fit the terminal window'),
        Option(long='quiet',
               short='q',
               kwargs={'quiet': True},
               description='Suppress output to stdout'),
        Option(long='verbose',
               short='v',
               kwargs={'verbose': True},
               description='Enable verbose output'),
        Option(short='h',
               long='help',
               kwargs={'show_help': True},
               description='Show help output'),
        Option(short='a',
               long='finclude',
               type=str,
               kwargs={'file_name_include_regex': ""},
               description='Only scan files whose names match this regex'),
        Option(short='p',
               long='fexclude',
               type=str,
               kwargs={'file_name_exclude_regex': ""},
               description='Do not scan files whose names match this regex'),
        Option(short='s',
               long='status',
               type=int,
               kwargs={'status_server_port': 0},
               description='Enable the status server on the specified port'),
        Option(long=None,
               short=None,
               type=binwalk.core.common.BlockFile,
               kwargs={'files': []}),

        # Hidden, API-only arguments
        Option(long="string",
               hidden=True,
               kwargs={'subclass': binwalk.core.common.StringFile}),
    ]

    KWARGS = [
        Kwarg(name='length', default=0),
        Kwarg(name='offset', default=0),
        Kwarg(name='base', default=0),
        Kwarg(name='block', default=0),
        Kwarg(name='status_server_port', default=0),
        Kwarg(name='swap_size', default=0),
        Kwarg(name='log_file', default=None),
        Kwarg(name='csv', default=False),
        Kwarg(name='format_to_terminal', default=False),
        Kwarg(name='quiet', default=False),
        Kwarg(name='verbose', default=False),
        Kwarg(name='files', default=[]),
        Kwarg(name='show_help', default=False),
        Kwarg(name='keep_going', default=False),
        Kwarg(name='subclass', default=io.FileIO),
        Kwarg(name='file_name_include_regex', default=None),
        Kwarg(name='file_name_exclude_regex', default=None),
    ]

    PRIMARY = False

    def load(self):
        self.threads_active = False
        self.target_files = []

        # A special case for when we're loaded into IDA
        if self.subclass == io.FileIO and binwalk.core.idb.LOADED_IN_IDA:
            self.subclass = binwalk.core.idb.IDBFileIO

        # Order is important with these two methods
        self._open_target_files()
        self._set_verbosity()

        # Build file name filter regex rules
        if self.file_name_include_regex:
            self.file_name_include_regex = re.compile(self.file_name_include_regex)
        if self.file_name_exclude_regex:
            self.file_name_exclude_regex = re.compile(self.file_name_exclude_regex)

        self.settings = binwalk.core.settings.Settings()
        self.display = binwalk.core.display.Display(log=self.log_file,
                                                    csv=self.csv,
                                                    quiet=self.quiet,
                                                    verbose=self.verbose,
                                                    fit_to_screen=self.format_to_terminal)

        if self.show_help:
            show_help()
            if not binwalk.core.idb.LOADED_IN_IDA:
                sys.exit(0)

        if self.status_server_port > 0:
            self.parent.status_server(self.status_server_port)

    def reset(self):
        pass

    def _set_verbosity(self):
        '''
        Sets the appropriate verbosity.
        Must be called after self._test_target_files so that self.target_files is properly set.
        '''
        # If more than one target file was specified, enable verbose mode; else, there is
        # nothing in some outputs to indicate which scan corresponds to which
        # file.
        if len(self.target_files) > 1 and not self.verbose:
            self.verbose = True

    def file_name_filter(self, fp):
        '''
        Checks to see if a file should be scanned based on file name include/exclude filters.
        Most useful for matryoshka scans where only certian files are desired.

        @fp - An instances of binwalk.common.BlockFile

        Returns True if the file should be scanned, False if not.
        '''
        if self.file_name_include_regex and not self.file_name_include_regex.search(fp.name):
            return False
        if self.file_name_exclude_regex and self.file_name_exclude_regex.search(fp.name):
            return False

        return True

    def open_file(self, fname, length=None, offset=None, swap=None, block=None, peek=None):
        '''
        Opens the specified file with all pertinent configuration settings.
        '''
        if length is None:
            length = self.length
        if offset is None:
            offset = self.offset
        if swap is None:
            swap = self.swap_size

        return binwalk.core.common.BlockFile(fname,
                                             subclass=self.subclass,
                                             length=length,
                                             offset=offset,
                                             swap=swap,
                                             block=block,
                                             peek=peek)

    def _open_target_files(self):
        '''
        Checks if the target files can be opened.
        Any files that cannot be opened are removed from the self.target_files list.
        '''
        # Validate the target files listed in target_files
        for tfile in self.files:
            # Ignore directories.
            if not self.subclass == io.FileIO or not os.path.isdir(tfile):
                # Make sure we can open the target files
                try:
                    fp = self.open_file(tfile)
                    fp.close()
                    self.target_files.append(tfile)
                except KeyboardInterrupt as e:
                    raise e
                except Exception as e:
                    self.error(description="Cannot open file %s (CWD: %s) : %s" % (tfile, os.getcwd(), str(e)))
