"""Amalgamation of xonsh.history package, made up of the following modules, in order:

* base
* dummy
* json
* sqlite
* main

"""

from sys import modules as _modules
from types import ModuleType as _ModuleType
from importlib import import_module as _import_module


class _LazyModule(_ModuleType):

    def __init__(self, pkg, mod, asname=None):
        '''Lazy module 'pkg.mod' in package 'pkg'.'''
        self.__dct__ = {
            'loaded': False,
            'pkg': pkg,  # pkg
            'mod': mod,  # pkg.mod
            'asname': asname,  # alias
            }

    @classmethod
    def load(cls, pkg, mod, asname=None):
        if mod in _modules:
            key = pkg if asname is None else mod
            return _modules[key]
        else:
            return cls(pkg, mod, asname)

    def __getattribute__(self, name):
        if name == '__dct__':
            return super(_LazyModule, self).__getattribute__(name)
        dct = self.__dct__
        mod = dct['mod']
        if dct['loaded']:
            m = _modules[mod]
        else:
            m = _import_module(mod)
            glbs = globals()
            pkg = dct['pkg']
            asname = dct['asname']
            if asname is None:
                glbs[pkg] = m = _modules[pkg]
            else:
                glbs[asname] = m
            dct['loaded'] = True
        return getattr(m, name)

#
# base
#
# -*- coding: utf-8 -*-
"""Base class of Xonsh History backends."""
uuid = _LazyModule.load('uuid', 'uuid')
xt = _LazyModule.load('xonsh', 'xonsh.tools', 'xt')
class History:
    """Xonsh history backend base class.

    History objects should be created via a subclass of History.

    Indexing
    --------
    History object acts like a sequence that can be indexed in a special
    way that adds extra functionality. Note that the most recent command
    is the last item in history.

    The index acts as a filter with two parts, command and argument,
    separated by comma. Based on the type of each part different
    filtering can be achieved,

        for the command part:

            - an int returns the command in that position.
            - a slice returns a list of commands.
            - a string returns the most recent command containing the string.

        for the argument part:

            - an int returns the argument of the command in that position.
            - a slice returns a part of the command based on the argument
              position.

    The argument part of the filter can be omitted but the command part is
    required. Command arguments are separated by white space.

    If the filtering produces only one result it is returned as a string
    else a list of strings is returned.

    Attributes
    ----------
    rtns : sequence of ints
        The return of the command (ie, 0 on success)
    inps : sequence of strings
        The command as typed by the user, including newlines
    tss : sequence of two-tuples of floats
        The timestamps of when the command started and finished, including
        fractions
    outs : sequence of strings
        The output of the command, if xonsh is configured to save it
    gc : A garbage collector or None
        The garbage collector

    In all of these sequences, index 0 is the oldest and -1 (the last item)
    is the newest.
    """
    def __init__(self, sessionid=None, **kwargs):
        """Represents a xonsh session's history.

        Parameters
        ----------
        sessionid : int, uuid, str, optional
            Current session identifier, will generate a new sessionid if not
            set.
        """
        self.sessionid = uuid.uuid4() if sessionid is None else sessionid
        self.gc = None
        self.buffer = None
        self.filename = None
        self.inps = None
        self.rtns = None
        self.tss = None
        self.outs = None
        self.last_cmd_rtn = None
        self.last_cmd_out = None

    def __len__(self):
        """Return the number of items in current session."""
        return len(list(self.items()))

    def __getitem__(self, item):
        """Retrieve history parts based on filtering rules,
        see ``History`` docs for more info. Accepts one of
        int, string, slice or tuple of length two.
        """
        if isinstance(item, tuple):
            cmd_pat, arg_pat = item
        else:
            cmd_pat, arg_pat = item, None
        cmds = [c['inp'] for c in self.items()]
        cmds = self._cmd_filter(cmds, cmd_pat)
        if arg_pat is not None:
            cmds = self._args_filter(cmds, arg_pat)
        cmds = list(cmds)
        if len(cmds) == 1:
            return cmds[0]
        else:
            return cmds

    def __setitem__(self, *args):
        raise PermissionError('You cannot change history! '
                              'you can create new though.')

    def append(self, cmd):
        """Append a command item into history.

        Parameters
        ----------
        cmd: dict
            A dict contains informations of a command. It should contains
            the following keys like ``inp``, ``rtn``, ``ts`` etc.
        """
        pass

    def flush(self, **kwargs):
        """Flush the history items to disk from a buffer."""
        pass

    def items(self):
        """Get history items of current session."""
        raise NotImplementedError

    def all_items(self):
        """Get all history items."""
        raise NotImplementedError

    def info(self):
        """A collection of information about the shell history.

        Returns
        -------
        dict or collections.OrderedDict
            Contains history information as str key pairs.
        """
        raise NotImplementedError

    def run_gc(self, size=None, blocking=True):
        """Run the garbage collector.

        Parameters
        ----------
        size: None or tuple of a int and a string
            Detemines the size and units of what would be allowed to remain.
        blocking: bool
            If set blocking, then wait until gc action finished.
        """
        pass

    @staticmethod
    def _cmd_filter(cmds, pat):
        if isinstance(pat, (int, slice)):
            s = xt.ensure_slice(pat)
            yield from xt.get_portions(cmds, s)
        elif xt.is_string(pat):
            for command in reversed(list(cmds)):
                if pat in command:
                    yield command
                    return
        else:
            raise TypeError('Command filter must be string, int or slice')

    @staticmethod
    def _args_filter(cmds, pat):
        args = None
        if isinstance(pat, (int, slice)):
            s = xt.ensure_slice(pat)
            for command in cmds:
                yield ' '.join(command.split()[s])
        else:
            raise TypeError('Argument filter must be int or slice')
        return args

#
# dummy
#
# -*- coding: utf-8 -*-
"""Implements the xonsh history backend."""
collections = _LazyModule.load('collections', 'collections')
# amalgamated xonsh.history.base
class DummyHistory(History):
    """A dummy implement of history backend."""
    def append(self, cmd):
        pass

    def items(self):
        yield {'inp': 'dummy in action', 'ts': 1464652800, 'ind': 0}

    def all_items(self):
        return self.items()

    def info(self):
        data = collections.OrderedDict()
        data['backend'] = 'dummy'
        data['sessionid'] = str(self.sessionid)
        return data

#
# json
#
# -*- coding: utf-8 -*-
"""Implements JSON version of xonsh history backend."""
os = _LazyModule.load('os', 'os')
time = _LazyModule.load('time', 'time')
builtins = _LazyModule.load('builtins', 'builtins')
# amalgamated collections
threading = _LazyModule.load('threading', 'threading')
cabc = _LazyModule.load('collections', 'collections.abc', 'cabc')
# amalgamated xonsh.history.base
# amalgamated xonsh.tools
xlj = _LazyModule.load('xonsh', 'xonsh.lazyjson', 'xlj')
def _xhj_gc_commands_to_rmfiles(hsize, files):
    """Return the history files to remove to get under the command limit."""
    rmfiles = []
    n = 0
    ncmds = 0
    for ts, fcmds, f in files[::-1]:
        if fcmds == 0:
            # we need to make sure that 'empty' history files don't hang around
            rmfiles.append((ts, fcmds, f))
        if ncmds + fcmds > hsize:
            break
        ncmds += fcmds
        n += 1
    rmfiles += files[:-n]
    return rmfiles


def _xhj_gc_files_to_rmfiles(hsize, files):
    """Return the history files to remove to get under the file limit."""
    rmfiles = files[:-hsize] if len(files) > hsize else []
    return rmfiles


def _xhj_gc_seconds_to_rmfiles(hsize, files):
    """Return the history files to remove to get under the age limit."""
    rmfiles = []
    now = time.time()
    for ts, _, f in files:
        if (now - ts) < hsize:
            break
        rmfiles.append((None, None, f))
    return rmfiles


def _xhj_gc_bytes_to_rmfiles(hsize, files):
    """Return the history files to remove to get under the byte limit."""
    rmfiles = []
    n = 0
    nbytes = 0
    for _, _, f in files[::-1]:
        fsize = os.stat(f).st_size
        if nbytes + fsize > hsize:
            break
        nbytes += fsize
        n += 1
    rmfiles = files[:-n]
    return rmfiles


def _xhj_get_history_files(sort=True, reverse=False):
    """Find and return the history files. Optionally sort files by
        modify time.
    """
    data_dir = builtins.__xonsh_env__.get('XONSH_DATA_DIR')
    data_dir = xt.expanduser_abs_path(data_dir)
    files = [os.path.join(data_dir, f) for f in os.listdir(data_dir)
             if f.startswith('xonsh-') and f.endswith('.json')]
    if sort:
        files.sort(key=lambda x: os.path.getmtime(x), reverse=reverse)
    return files


class JsonHistoryGC(threading.Thread):
    """Shell history garbage collection."""

    def __init__(self, wait_for_shell=True, size=None, *args, **kwargs):
        """Thread responsible for garbage collecting old history.

        May wait for shell (and for xonshrc to have been loaded) to start work.
        """
        super().__init__(*args, **kwargs)
        self.daemon = True
        self.size = size
        self.wait_for_shell = wait_for_shell
        self.start()
        self.gc_units_to_rmfiles = {'commands': _xhj_gc_commands_to_rmfiles,
                                    'files': _xhj_gc_files_to_rmfiles,
                                    's': _xhj_gc_seconds_to_rmfiles,
                                    'b': _xhj_gc_bytes_to_rmfiles}

    def run(self):
        while self.wait_for_shell:
            time.sleep(0.01)
        env = builtins.__xonsh_env__  # pylint: disable=no-member
        if self.size is None:
            hsize, units = env.get('XONSH_HISTORY_SIZE')
        else:
            hsize, units = xt.to_history_tuple(self.size)
        files = self.files(only_unlocked=True)
        rmfiles_fn = self.gc_units_to_rmfiles.get(units)
        if rmfiles_fn is None:
            raise ValueError('Units type {0!r} not understood'.format(units))

        for _, _, f in rmfiles_fn(hsize, files):
            try:
                os.remove(f)
            except OSError:
                pass

    def files(self, only_unlocked=False):
        """Find and return the history files. Optionally locked files may be
        excluded.

        This is sorted by the last closed time. Returns a list of
        (timestamp, number of cmds, file name) tuples.
        """
        # pylint: disable=no-member
        env = getattr(builtins, '__xonsh_env__', None)
        if env is None:
            return []

        fs = _xhj_get_history_files(sort=False)
        files = []
        for f in fs:
            try:
                if os.path.getsize(f) == 0:
                    # collect empty files (for gc)
                    files.append((time.time(), 0, f))
                    continue
                lj = xlj.LazyJSON(f, reopen=False)
                if only_unlocked and lj['locked']:
                    continue
                # info: closing timestamp, number of commands, filename
                files.append((lj['ts'][1] or time.time(),
                              len(lj.sizes['cmds']) - 1,
                              f))
                lj.close()
            except (IOError, OSError, ValueError):
                continue
        files.sort()
        return files


class JsonHistoryFlusher(threading.Thread):
    """Flush shell history to disk periodically."""

    def __init__(self, filename, buffer, queue, cond, at_exit=False, *args,
                 **kwargs):
        """Thread for flushing history."""
        super(JsonHistoryFlusher, self).__init__(*args, **kwargs)
        self.filename = filename
        self.buffer = buffer
        self.queue = queue
        queue.append(self)
        self.cond = cond
        self.at_exit = at_exit
        if at_exit:
            self.dump()
            queue.popleft()
        else:
            self.start()

    def run(self):
        with self.cond:
            self.cond.wait_for(self.i_am_at_the_front)
            self.dump()
            self.queue.popleft()

    def i_am_at_the_front(self):
        """Tests if the flusher is at the front of the queue."""
        return self is self.queue[0]

    def dump(self):
        """Write the cached history to external storage."""
        opts = builtins.__xonsh_env__.get('HISTCONTROL')
        last_inp = None
        cmds = []
        for cmd in self.buffer:
            if 'ignoredups' in opts and cmd['inp'] == last_inp:
                # Skipping dup cmd
                continue
            if 'ignoreerr' in opts and cmd['rtn'] != 0:
                # Skipping failed cmd
                continue
            cmds.append(cmd)
            last_inp = cmd['inp']
        with open(self.filename, 'r', newline='\n') as f:
            hist = xlj.LazyJSON(f).load()
        load_hist_len = len(hist['cmds'])
        hist['cmds'].extend(cmds)
        if self.at_exit:
            hist['ts'][1] = time.time()  # apply end time
            hist['locked'] = False
        if not builtins.__xonsh_env__.get('XONSH_STORE_STDOUT', False):
            [cmd.pop('out') for cmd in hist['cmds'][load_hist_len:]
                if 'out' in cmd]
        with open(self.filename, 'w', newline='\n') as f:
            xlj.ljdump(hist, f, sort_keys=True)


class JsonCommandField(cabc.Sequence):
    """A field in the 'cmds' portion of history."""

    def __init__(self, field, hist, default=None):
        """Represents a field in the 'cmds' portion of history.

        Will query the buffer for the relevant data, if possible. Otherwise it
        will lazily acquire data from the file.

        Parameters
        ----------
        field : str
            The name of the field to query.
        hist : History object
            The history object to query.
        default : optional
            The default value to return if key is not present.
        """
        self.field = field
        self.hist = hist
        self.default = default

    def __len__(self):
        return len(self.hist)

    def __getitem__(self, key):
        size = len(self)
        if isinstance(key, slice):
            return [self[i] for i in range(*key.indices(size))]
        elif not isinstance(key, int):
            raise IndexError(
                'JsonCommandField may only be indexed by int or slice.')
        elif size == 0:
            raise IndexError('JsonCommandField is empty.')
        # now we know we have an int
        key = size + key if key < 0 else key  # ensure key is non-negative
        bufsize = len(self.hist.buffer)
        if size - bufsize <= key:  # key is in buffer
            return self.hist.buffer[key + bufsize - size].get(
                self.field, self.default)
        # now we know we have to go into the file
        queue = self.hist._queue
        queue.append(self)
        with self.hist._cond:
            self.hist._cond.wait_for(self.i_am_at_the_front)
            with open(self.hist.filename, 'r', newline='\n') as f:
                lj = xlj.LazyJSON(f, reopen=False)
                rtn = lj['cmds'][key].get(self.field, self.default)
                if isinstance(rtn, xlj.LJNode):
                    rtn = rtn.load()
            queue.popleft()
        return rtn

    def i_am_at_the_front(self):
        """Tests if the command field is at the front of the queue."""
        return self is self.hist._queue[0]


class JsonHistory(History):
    """Xonsh history backend implemented with JSON files.

    JsonHistory implements two extra actions: ``diff``, and ``replay``.
    """

    def __init__(self, filename=None, sessionid=None, buffersize=100, gc=True,
                 **meta):
        """Represents a xonsh session's history as an in-memory buffer that is
        periodically flushed to disk.

        Parameters
        ----------
        filename : str, optional
            Location of history file, defaults to
            ``$XONSH_DATA_DIR/xonsh-{sessionid}.json``.
        sessionid : int, uuid, str, optional
            Current session identifier, will generate a new sessionid if not
            set.
        buffersize : int, optional
            Maximum buffersize in memory.
        meta : optional
            Top-level metadata to store along with the history. The kwargs
            'cmds' and 'sessionid' are not allowed and will be overwritten.
        gc : bool, optional
            Run garbage collector flag.
        """
        super().__init__(sessionid=sessionid, **meta)
        if filename is None:
            # pylint: disable=no-member
            data_dir = builtins.__xonsh_env__.get('XONSH_DATA_DIR')
            data_dir = os.path.expanduser(data_dir)
            self.filename = os.path.join(
                data_dir, 'xonsh-{0}.json'.format(self.sessionid))
        else:
            self.filename = filename
        self.buffer = []
        self.buffersize = buffersize
        self._queue = collections.deque()
        self._cond = threading.Condition()
        self._len = 0
        self.last_cmd_out = None
        self.last_cmd_rtn = None
        meta['cmds'] = []
        meta['sessionid'] = str(self.sessionid)
        with open(self.filename, 'w', newline='\n') as f:
            xlj.ljdump(meta, f, sort_keys=True)
        self.gc = JsonHistoryGC() if gc else None
        # command fields that are known
        self.tss = JsonCommandField('ts', self)
        self.inps = JsonCommandField('inp', self)
        self.outs = JsonCommandField('out', self)
        self.rtns = JsonCommandField('rtn', self)

    def __len__(self):
        return self._len

    def append(self, cmd):
        """Appends command to history. Will periodically flush the history to file.

        Parameters
        ----------
        cmd : dict
            Command dictionary that should be added to the ordered history.

        Returns
        -------
        hf : JsonHistoryFlusher or None
            The thread that was spawned to flush history
        """
        self.buffer.append(cmd)
        self._len += 1  # must come before flushing
        if len(self.buffer) >= self.buffersize:
            hf = self.flush()
        else:
            hf = None
        return hf

    def flush(self, at_exit=False):
        """Flushes the current command buffer to disk.

        Parameters
        ----------
        at_exit : bool, optional
            Whether the JsonHistoryFlusher should act as a thread in the
            background, or execute immeadiately and block.

        Returns
        -------
        hf : JsonHistoryFlusher or None
            The thread that was spawned to flush history
        """
        if len(self.buffer) == 0:
            return
        hf = JsonHistoryFlusher(self.filename, tuple(self.buffer), self._queue,
                                self._cond, at_exit=at_exit)
        self.buffer.clear()
        return hf

    def items(self):
        """Display history items of current session."""
        for item, tss in zip(self.inps, self.tss):
            yield {'inp': item.rstrip(), 'ts': tss[0]}

    def all_items(self, **kwargs):
        """
        Returns all history as found in XONSH_DATA_DIR.

        yeild format: {'inp': cmd, 'rtn': 0, ...}
        """
        while self.gc and self.gc.is_alive():
            time.sleep(0.011)  # gc sleeps for 0.01 secs, sleep a beat longer
        for f in _xhj_get_history_files():
            try:
                json_file = xlj.LazyJSON(f, reopen=False)
            except ValueError:
                # Invalid json file
                continue
            commands = json_file.load()['cmds']
            for c in commands:
                yield {'inp': c['inp'].rstrip(), 'ts': c['ts'][0]}
        # all items should also include session items
        yield from self.items()

    def info(self):
        data = collections.OrderedDict()
        data['backend'] = 'json'
        data['sessionid'] = str(self.sessionid)
        data['filename'] = self.filename
        data['length'] = len(self)
        data['buffersize'] = self.buffersize
        data['bufferlength'] = len(self.buffer)
        envs = builtins.__xonsh_env__
        data['gc options'] = envs.get('XONSH_HISTORY_SIZE')
        return data

    def run_gc(self, size=None, blocking=True):
        self.gc = JsonHistoryGC(wait_for_shell=False, size=size)
        if blocking:
            while self.gc.is_alive():
                continue

#
# sqlite
#
# -*- coding: utf-8 -*-
"""Implements the xonsh history backend via sqlite3."""
# amalgamated builtins
# amalgamated collections
json = _LazyModule.load('json', 'json')
# amalgamated os
sqlite3 = _LazyModule.load('sqlite3', 'sqlite3')
sys = _LazyModule.load('sys', 'sys')
# amalgamated threading
# amalgamated time
# amalgamated xonsh.history.base
# amalgamated xonsh.tools
def _xh_sqlite_get_file_name():
    envs = builtins.__xonsh_env__
    file_name = envs.get('XONSH_HISTORY_SQLITE_FILE')
    if not file_name:
        data_dir = envs.get('XONSH_DATA_DIR')
        file_name = os.path.join(data_dir, 'xonsh-history.sqlite')
    return xt.expanduser_abs_path(file_name)


def _xh_sqlite_get_conn(filename=None):
    if filename is None:
        filename = _xh_sqlite_get_file_name()
    return sqlite3.connect(filename)


def _xh_sqlite_create_history_table(cursor):
    """Create Table for history items.

    Columns:
        info - JSON formated, reserved for future extension.
    """
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS xonsh_history
             (inp TEXT,
              rtn INTEGER,
              tsb REAL,
              tse REAL,
              sessionid TEXT,
              out TEXT,
              info TEXT
             )
    """)


def _xh_sqlite_insert_command(cursor, cmd, sessionid, store_stdout):
    sql = 'INSERT INTO xonsh_history (inp, rtn, tsb, tse, sessionid'
    tss = cmd.get('ts', [None, None])
    params = [
        cmd['inp'].rstrip(),
        cmd['rtn'],
        tss[0],
        tss[1],
        sessionid,
    ]
    if store_stdout and 'out' in cmd:
        sql += ', out'
        params.append(cmd['out'])
    if 'info' in cmd:
        sql += ', info'
        info = json.dumps(cmd['info'])
        params.append(info)
    sql += ') VALUES (' + ('?, ' * len(params)).rstrip(', ') + ')'
    cursor.execute(sql, tuple(params))


def _xh_sqlite_get_count(cursor, sessionid=None):
    sql = 'SELECT count(*) FROM xonsh_history '
    params = []
    if sessionid is not None:
        sql += 'WHERE sessionid = ? '
        params.append(str(sessionid))
    cursor.execute(sql, tuple(params))
    return cursor.fetchone()[0]


def _xh_sqlite_get_records(cursor, sessionid=None, limit=None, reverse=False):
    sql = 'SELECT inp, tsb, rtn FROM xonsh_history '
    params = []
    if sessionid is not None:
        sql += 'WHERE sessionid = ? '
        params.append(sessionid)
    sql += 'ORDER BY tsb '
    if reverse:
        sql += 'DESC '
    if limit is not None:
        sql += 'LIMIT %d ' % limit
    cursor.execute(sql, tuple(params))
    return cursor.fetchall()


def _xh_sqlite_delete_records(cursor, size_to_keep):
    sql = 'SELECT min(tsb) FROM ('
    sql += 'SELECT tsb FROM xonsh_history ORDER BY tsb DESC '
    sql += 'LIMIT %d)' % size_to_keep
    cursor.execute(sql)
    result = cursor.fetchone()
    if not result:
        return
    max_tsb = result[0]
    sql = 'DELETE FROM xonsh_history WHERE tsb < ?'
    result = cursor.execute(sql, (max_tsb,))
    return result.rowcount


def xh_sqlite_append_history(cmd, sessionid, store_stdout, filename=None):
    with _xh_sqlite_get_conn(filename=filename) as conn:
        c = conn.cursor()
        _xh_sqlite_create_history_table(c)
        _xh_sqlite_insert_command(c, cmd, sessionid, store_stdout)
        conn.commit()


def xh_sqlite_get_count(sessionid=None, filename=None):
    with _xh_sqlite_get_conn(filename=filename) as conn:
        c = conn.cursor()
        return _xh_sqlite_get_count(c, sessionid=sessionid)


def xh_sqlite_items(sessionid=None, filename=None):
    with _xh_sqlite_get_conn(filename=filename) as conn:
        c = conn.cursor()
        _xh_sqlite_create_history_table(c)
        return _xh_sqlite_get_records(c, sessionid=sessionid)


def xh_sqlite_delete_items(size_to_keep, filename=None):
    with _xh_sqlite_get_conn(filename=filename) as conn:
        c = conn.cursor()
        _xh_sqlite_create_history_table(c)
        return _xh_sqlite_delete_records(c, size_to_keep)


class SqliteHistoryGC(threading.Thread):
    """Shell history garbage collection."""

    def __init__(self, wait_for_shell=True, size=None, filename=None,
                 *args, **kwargs):
        """Thread responsible for garbage collecting old history.

        May wait for shell (and for xonshrc to have been loaded) to start work.
        """
        super().__init__(*args, **kwargs)
        self.daemon = True
        self.filename = filename
        self.size = size
        self.wait_for_shell = wait_for_shell
        self.start()

    def run(self):
        while self.wait_for_shell:
            time.sleep(0.01)
        if self.size is not None:
            hsize, units = xt.to_history_tuple(self.size)
        else:
            envs = builtins.__xonsh_env__
            hsize, units = envs.get('XONSH_HISTORY_SIZE')
        if units != 'commands':
            print('sqlite backed history gc currently only supports '
                  '"commands" as units', file=sys.stderr)
            return
        if hsize < 0:
            return
        xh_sqlite_delete_items(hsize, filename=self.filename)


class SqliteHistory(History):
    """Xonsh history backend implemented with sqlite3."""

    def __init__(self, gc=True, filename=None, **kwargs):
        super().__init__(**kwargs)
        if filename is None:
            filename = _xh_sqlite_get_file_name()
        self.filename = filename
        self.gc = SqliteHistoryGC() if gc else None
        self._last_hist_inp = None
        self.inps = []
        self.rtns = []
        self.outs = []
        self.tss = []

    def append(self, cmd):
        envs = builtins.__xonsh_env__
        opts = envs.get('HISTCONTROL')
        inp = cmd['inp'].rstrip()
        self.inps.append(inp)
        store_stdout = envs.get('XONSH_STORE_STDOUT', False)
        if store_stdout:
            self.outs.append(cmd.get('out'))
        else:
            self.outs.append(None)
        self.rtns.append(cmd['rtn'])
        self.tss.append(cmd.get('ts', (None, None)))

        opts = envs.get('HISTCONTROL')
        if 'ignoredups' in opts and inp == self._last_hist_inp:
            # Skipping dup cmd
            return
        if 'ignoreerr' in opts and cmd['rtn'] != 0:
            # Skipping failed cmd
            return
        self._last_hist_inp = inp
        xh_sqlite_append_history(
            cmd, str(self.sessionid), store_stdout,
            filename=self.filename)

    def all_items(self):
        """Display all history items."""
        for item in xh_sqlite_items(filename=self.filename):
            yield {'inp': item[0], 'ts': item[1], 'rtn': item[2]}

    def items(self):
        """Display history items of current session."""
        for item in xh_sqlite_items(
                sessionid=str(self.sessionid), filename=self.filename):
            yield {'inp': item[0], 'ts': item[1], 'rtn': item[2]}

    def info(self):
        data = collections.OrderedDict()
        data['backend'] = 'sqlite'
        data['sessionid'] = str(self.sessionid)
        data['filename'] = self.filename
        data['session items'] = xh_sqlite_get_count(
            sessionid=self.sessionid, filename=self.filename)
        data['all items'] = xh_sqlite_get_count(filename=self.filename)
        envs = builtins.__xonsh_env__
        data['gc options'] = envs.get('XONSH_HISTORY_SIZE')
        return data

    def run_gc(self, size=None, blocking=True):
        self.gc = SqliteHistoryGC(wait_for_shell=False, size=size)
        if blocking:
            while self.gc.is_alive():
                continue

#
# main
#
# -*- coding: utf-8 -*-
"""Main entry points of the xonsh history."""
argparse = _LazyModule.load('argparse', 'argparse')
# amalgamated builtins
datetime = _LazyModule.load('datetime', 'datetime')
functools = _LazyModule.load('functools', 'functools')
# amalgamated json
# amalgamated os
# amalgamated sys
# amalgamated xonsh.history.dummy
# amalgamated xonsh.history.json
# amalgamated xonsh.history.sqlite
xdh = _LazyModule.load('xonsh', 'xonsh.diff_history', 'xdh')
xla = _LazyModule.load('xonsh', 'xonsh.lazyasd', 'xla')
# amalgamated xonsh.tools
HISTORY_BACKENDS = {
    'dummy': DummyHistory,
    'json': JsonHistory,
    'sqlite': SqliteHistory,
}


def construct_history(**kwargs):
    """Construct the history backend object."""
    env = builtins.__xonsh_env__
    backend = env.get('XONSH_HISTORY_BACKEND')
    if backend not in HISTORY_BACKENDS:
        print('Unknown history backend: {}. Using JSON version'.format(
            backend), file=sys.stderr)
        kls_history = JsonHistory
    else:
        kls_history = HISTORY_BACKENDS[backend]
    return kls_history(**kwargs)


def _xh_session_parser(hist=None, **kwargs):
    """Returns history items of current session."""
    if hist is None:
        hist = builtins.__xonsh_history__
    return hist.items()


def _xh_all_parser(hist=None, **kwargs):
    """Returns all history items."""
    if hist is None:
        hist = builtins.__xonsh_history__
    return hist.all_items()


def _xh_find_histfile_var(file_list, default=None):
    """Return the path of the history file
    from the value of the envvar HISTFILE.
    """
    for f in file_list:
        f = xt.expanduser_abs_path(f)
        if not os.path.isfile(f):
            continue
        with open(f, 'r') as rc_file:
            for line in rc_file:
                if line.startswith('HISTFILE='):
                    hist_file = line.split('=', 1)[1].strip('\'"\n')
                    hist_file = xt.expanduser_abs_path(hist_file)
                    if os.path.isfile(hist_file):
                        return hist_file
    else:
        if default:
            default = xt.expanduser_abs_path(default)
            if os.path.isfile(default):
                return default


def _xh_bash_hist_parser(location=None, **kwargs):
    """Yield commands from bash history file"""
    if location is None:
        location = _xh_find_histfile_var([os.path.join('~', '.bashrc'),
                                         os.path.join('~', '.bash_profile')],
                                         os.path.join('~', '.bash_history'))
    if location:
        with open(location, 'r', errors='backslashreplace') as bash_hist:
            for ind, line in enumerate(bash_hist):
                yield {'inp': line.rstrip(), 'ts': 0.0, 'ind': ind}
    else:
        print("No bash history file", file=sys.stderr)


def _xh_zsh_hist_parser(location=None, **kwargs):
    """Yield commands from zsh history file"""
    if location is None:
        location = _xh_find_histfile_var([os.path.join('~', '.zshrc'),
                                         os.path.join('~', '.zprofile')],
                                         os.path.join('~', '.zsh_history'))
    if location:
        with open(location, 'r', errors='backslashreplace') as zsh_hist:
            for ind, line in enumerate(zsh_hist):
                if line.startswith(':'):
                    try:
                        start_time, command = line.split(';', 1)
                    except ValueError:
                        # Invalid history entry
                        continue
                    try:
                        start_time = float(start_time.split(':')[1])
                    except ValueError:
                        start_time = 0.0
                    yield {'inp': command.rstrip(), 'ts': start_time, 'ind': ind}
                else:
                    yield {'inp': line.rstrip(), 'ts': 0.0, 'ind': ind}

    else:
        print("No zsh history file found", file=sys.stderr)


def _xh_filter_ts(commands, start_time, end_time):
    """Yield only the commands between start and end time."""
    for cmd in commands:
        if start_time <= cmd['ts'] < end_time:
            yield cmd


def _xh_get_history(session='session', *, slices=None, datetime_format=None,
                    start_time=None, end_time=None, location=None):
    """Get the requested portion of shell history.

    Parameters
    ----------
    session: {'session', 'all', 'xonsh', 'bash', 'zsh'}
        The history session to get.
    slices : list of slice-like objects, optional
        Get only portions of history.
    start_time, end_time: float, optional
        Filter commands by timestamp.
    location: string, optional
        The history file location (bash or zsh)

    Returns
    -------
    generator
       A filtered list of commands
    """
    cmds = []
    for i, item in enumerate(_XH_HISTORY_SESSIONS[session](location=location)):
        item['ind'] = i
        cmds.append(item)
    if slices:
        # transform/check all slices
        slices = [xt.ensure_slice(s) for s in slices]
        cmds = xt.get_portions(cmds, slices)
    if start_time or end_time:
        if start_time is None:
            start_time = 0.0
        else:
            start_time = xt.ensure_timestamp(start_time, datetime_format)
        if end_time is None:
            end_time = float('inf')
        else:
            end_time = xt.ensure_timestamp(end_time, datetime_format)
        cmds = _xh_filter_ts(cmds, start_time, end_time)
    return cmds


def _xh_show_history(hist, ns, stdout=None, stderr=None):
    """Show the requested portion of shell history.
    Accepts same parameters with `_xh_get_history`.
    """
    try:
        commands = _xh_get_history(ns.session,
                                   slices=ns.slices,
                                   start_time=ns.start_time,
                                   end_time=ns.end_time,
                                   datetime_format=ns.datetime_format)
    except ValueError as err:
        print("history: error: {}".format(err), file=stderr)
        return
    if ns.reverse:
        commands = reversed(list(commands))
    if ns.numerate and ns.timestamp:
        for c in commands:
            dt = datetime.datetime.fromtimestamp(c['ts'])
            print('{}:({}) {}'.format(c['ind'], xt.format_datetime(dt), c['inp']),
                  file=stdout)
    elif ns.numerate:
        for c in commands:
            print('{}: {}'.format(c['ind'], c['inp']), file=stdout)
    elif ns.timestamp:
        for c in commands:
            dt = datetime.datetime.fromtimestamp(c['ts'])
            print('({}) {}'.format(xt.format_datetime(dt), c['inp']),
                  file=stdout)
    else:
        for c in commands:
            print(c['inp'], file=stdout)


@xla.lazyobject
def _XH_HISTORY_SESSIONS():
    return {'session': _xh_session_parser,
            'xonsh': _xh_all_parser,
            'all': _xh_all_parser,
            'zsh': _xh_zsh_hist_parser,
            'bash': _xh_bash_hist_parser}


_XH_MAIN_ACTIONS = {'show', 'id', 'file', 'info', 'diff', 'gc'}


@functools.lru_cache()
def _xh_create_parser():
    """Create a parser for the "history" command."""
    p = argparse.ArgumentParser(prog='history',
                                description="try 'history <command> --help' "
                                            'for more info')
    subp = p.add_subparsers(title='commands', dest='action')
    # session action
    show = subp.add_parser('show', prefix_chars='-+',
                           help='display history of a session, default command')
    show.add_argument('-r', dest='reverse', default=False,
                      action='store_true', help='reverses the direction')
    show.add_argument('-n', dest='numerate', default=False,
                      action='store_true', help='numerate each command')
    show.add_argument('-t', dest='timestamp', default=False,
                      action='store_true', help='show command timestamps')
    show.add_argument('-T', dest='end_time', default=None,
                      help='show only commands before timestamp')
    show.add_argument('+T', dest='start_time', default=None,
                      help='show only commands after timestamp')
    show.add_argument('-f', dest='datetime_format', default=None,
                      help='the datetime format to be used for'
                           'filtering and printing')
    show.add_argument('session', nargs='?', choices=_XH_HISTORY_SESSIONS.keys(),
                      default='session',
                      metavar='session',
                      help='{} (default: current session, all is an alias for xonsh)'
                           ''.format(', '.join(map(repr, _XH_HISTORY_SESSIONS.keys()))))
    show.add_argument('slices', nargs='*', default=None, metavar='slice',
                      help='integer or slice notation')
    # 'id' subcommand
    subp.add_parser('id', help='display the current session id')
    # 'file' subcommand
    subp.add_parser('file', help='display the current history filename')
    # 'info' subcommand
    info = subp.add_parser('info', help=('display information about the '
                                         'current history'))
    info.add_argument('--json', dest='json', default=False,
                      action='store_true', help='print in JSON format')

    # gc
    gcp = subp.add_parser(
        'gc', help='launches a new history garbage collector')
    gcp.add_argument('--size', nargs=2, dest='size', default=None,
                     help=('next two arguments represent the history size and '
                           'units; e.g. "--size 8128 commands"'))
    bgcp = gcp.add_mutually_exclusive_group()
    bgcp.add_argument('--blocking', dest='blocking', default=True,
                      action='store_true',
                      help=('ensures that the gc blocks the main thread, '
                            'default True'))
    bgcp.add_argument('--non-blocking', dest='blocking', action='store_false',
                      help='makes the gc non-blocking, and thus return sooner')

    hist = builtins.__xonsh_history__
    if isinstance(hist, JsonHistory):
        # add actions belong only to JsonHistory
        diff = subp.add_parser('diff', help='diff two xonsh history files')
        xdh.dh_create_parser(p=diff)

        import xonsh.replay as xrp
        replay = subp.add_parser('replay', help='replay a xonsh history file')
        xrp.replay_create_parser(p=replay)
        _XH_MAIN_ACTIONS.add('replay')

    return p


def _xh_parse_args(args):
    """Prepare and parse arguments for the history command.

    Add default action for ``history`` and
    default session for ``history show``.
    """
    parser = _xh_create_parser()
    if not args:
        args = ['show', 'session']
    elif args[0] not in _XH_MAIN_ACTIONS and args[0] not in ('-h', '--help'):
        args = ['show', 'session'] + args
    if args[0] == 'show':
        if not any(a in _XH_HISTORY_SESSIONS for a in args):
            args.insert(1, 'session')
        ns, slices = parser.parse_known_args(args)
        if slices:
            if not ns.slices:
                ns.slices = slices
            else:
                ns.slices.extend(slices)
    else:
        ns = parser.parse_args(args)
    return ns


def history_main(args=None, stdin=None, stdout=None, stderr=None):
    """This is the history command entry point."""
    hist = builtins.__xonsh_history__
    ns = _xh_parse_args(args)
    if not ns or not ns.action:
        return
    if ns.action == 'show':
        _xh_show_history(hist, ns, stdout=stdout, stderr=stderr)
    elif ns.action == 'info':
        data = hist.info()
        if ns.json:
            s = json.dumps(data)
            print(s, file=stdout)
        else:
            lines = ['{0}: {1}'.format(k, v) for k, v in data.items()]
            print('\n'.join(lines), file=stdout)
    elif ns.action == 'id':
        if not hist.sessionid:
            return
        print(str(hist.sessionid), file=stdout)
    elif ns.action == 'file':
        if not hist.filename:
            return
        print(str(hist.filename), file=stdout)
    elif ns.action == 'gc':
        hist.run_gc(size=ns.size, blocking=ns.blocking)
    elif ns.action == 'diff':
        if isinstance(hist, JsonHistory):
            xdh.dh_main_action(ns)
    elif ns.action == 'replay':
        if isinstance(hist, JsonHistory):
            import xonsh.replay as xrp
            xrp.replay_main_action(hist, ns, stdout=stdout, stderr=stderr)
    else:
        print('Unknown history action {}'.format(ns.action), file=sys.stderr)

