import os, queue, concurrent.futures

class ThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
    """ThreadPoolExecutor which allows setting max. work queue size"""
    def __init__(self, max_workers=0, thread_name_prefix='', max_queue_size=0):
        #super().__init__(max_workers or os.cpu_count() or 1, thread_name_prefix)
        super().__init__(max_workers or os.cpu_count() or 1)
        self.max_queue_size = max_queue_size or self._max_workers * 10
        if self.max_queue_size > 0:
            self._work_queue = queue.Queue(self.max_queue_size)
        self.max_queue_size_reached = 0

    def submit(self, fn, *args, **kwargs):
        """Submits a callable to be executed with the given arguments.

        Count maximum reached work queue size in ThreadPoolExecutor.max_queue_size_reached.
        """
        future = super().submit(fn, *args, **kwargs)
        work_queue_size = self._work_queue.qsize()
        if work_queue_size > self.max_queue_size_reached:
            self.max_queue_size_reached = work_queue_size
        return future


#!/usr/bin/env python3

import sys, time, datetime, math, logging, signal

import numpy
import simplesoapy
from simplespectral import zeros

from soapypower import psd, writer

logger = logging.getLogger(__name__)
_shutdown = False

def _shutdown_handler(sig, frame):
    """Set global _shutdown flag when receiving SIGTERM or SIGINT signals"""
    global _shutdown
    _shutdown = True

# Register signals with _shutdown_handler
signal.signal(signal.SIGTERM, _shutdown_handler)
signal.signal(signal.SIGINT, _shutdown_handler)

if sys.platform == 'win32':
    signal.signal(signal.SIGBREAK, _shutdown_handler)

class SoapyPower:
    """SoapySDR spectrum analyzer"""
    def __init__(self, soapy_args='', sample_rate=2.00e6, bandwidth=0, corr=0, gain=20.7,
                 auto_gain=False, channel=0, antenna='', settings=None,
                 force_sample_rate=False, force_bandwidth=False,
                 output=sys.stdout, output_format='rtl_power'):
        self.device = simplesoapy.SoapyDevice(
            soapy_args=soapy_args, sample_rate=sample_rate, bandwidth=bandwidth, corr=corr,
            gain=gain, auto_gain=auto_gain, channel=channel, antenna=antenna, settings=settings,
            force_sample_rate=force_sample_rate, force_bandwidth=force_bandwidth
        )

        self._output = output
        self._output_format = output_format

        self._buffer = None
        self._buffer_repeats = None
        self._base_buffer_size = None
        self._max_buffer_size = None
        self._bins = None
        self._repeats = None
        self._tune_delay = None
        self._reset_stream = None
        self._psd = None
        self._writer = None

    def nearest_freq(self, freq, bin_size):
        """Return nearest frequency based on bin size"""
        return round(freq / bin_size) * bin_size

    def nearest_bins(self, bins, even=False, pow2=False):
        """Return nearest number of FFT bins (even or power of two)"""
        if pow2:
            bins_log2 = math.log(bins, 2)
            if bins_log2 % 1 != 0:
                bins = 2**math.ceil(bins_log2)
                logger.warning('number of FFT bins should be power of two, changing to {}'.format(bins))
        elif even:
            if bins % 2 != 0:
                bins = math.ceil(bins / 2) * 2
                logger.warning('number of FFT bins should be even, changing to {}'.format(bins))

        return bins

    def nearest_overlap(self, overlap, bins):
        """Return nearest overlap/crop factor based on number of bins"""
        bins_overlap = overlap * bins
        if bins_overlap % 2 != 0:
            bins_overlap = math.ceil(bins_overlap / 2) * 2
            overlap = bins_overlap / bins
            logger.warning('number of overlapping FFT bins should be even, '
                           'changing overlap/crop factor to {:.5f}'.format(overlap))
        return overlap

    def bin_size_to_bins(self, bin_size):
        """Convert bin size [Hz] to number of FFT bins"""
        return math.ceil(self.device.sample_rate / bin_size)

    def bins_to_bin_size(self, bins):
        """Convert number of FFT bins to bin size [Hz]"""
        return self.device.sample_rate / bins

    def time_to_repeats(self, bins, integration_time):
        """Convert integration time to number of repeats"""
        return math.ceil((self.device.sample_rate * integration_time) / bins)

    def repeats_to_time(self, bins, repeats):
        """Convert number of repeats to integration time"""
        return (repeats * bins) / self.device.sample_rate

    def freq_plan(self, min_freq, max_freq, bins, overlap=0, quiet=False):
        """Returns list of frequencies for frequency hopping"""
        bin_size = self.bins_to_bin_size(bins)
        bins_crop = round((1 - overlap) * bins)
        sample_rate_crop = (1 - overlap) * self.device.sample_rate

        freq_range = max_freq - min_freq
        hopping = True if freq_range >= sample_rate_crop else False
        hop_size = self.nearest_freq(sample_rate_crop, bin_size)
        hops = math.ceil(freq_range / hop_size) if hopping else 1
        min_center_freq = min_freq + (hop_size / 2) if hopping else min_freq + (freq_range / 2)
        max_center_freq = min_center_freq + ((hops - 1) * hop_size)

        freq_list = [min_center_freq + (i * hop_size) for i in range(hops)]

        if not quiet:
            logger.info('overlap: {:.5f}'.format(overlap))
            logger.info('bin_size: {:.2f} Hz'.format(bin_size))
            logger.info('bins: {}'.format(bins))
            logger.info('bins (after crop): {}'.format(bins_crop))
            logger.info('sample_rate: {:.3f} MHz'.format(self.device.sample_rate / 1e6))
            logger.info('sample_rate (after crop): {:.3f} MHz'.format(sample_rate_crop / 1e6))
            logger.info('freq_range: {:.3f} MHz'.format(freq_range / 1e6))
            logger.info('hopping: {}'.format('YES' if hopping else 'NO'))
            logger.info('hop_size: {:.3f} MHz'.format(hop_size / 1e6))
            logger.info('hops: {}'.format(hops))
            logger.info('min_center_freq: {:.3f} MHz'.format(min_center_freq / 1e6))
            logger.info('max_center_freq: {:.3f} MHz'.format(max_center_freq / 1e6))
            logger.info('min_freq (after crop): {:.3f} MHz'.format((min_center_freq - (hop_size / 2)) / 1e6))
            logger.info('max_freq (after crop): {:.3f} MHz'.format((max_center_freq + (hop_size / 2)) / 1e6))

            logger.debug('Frequency hops table:')
            logger.debug('  {:8s}      {:8s}      {:8s}'.format('Min:', 'Center:', 'Max:'))
            for f in freq_list:
                logger.debug('  {:8.3f} MHz  {:8.3f} MHz  {:8.3f} MHz'.format(
                    (f - (self.device.sample_rate / 2)) / 1e6,
                    f / 1e6,
                    (f + (self.device.sample_rate / 2)) / 1e6,
                ))

        return freq_list

    def create_buffer(self, bins, repeats, base_buffer_size, max_buffer_size=0):
        """Create buffer for reading samples"""
        samples = bins * repeats
        buffer_repeats = 1
        buffer_size = math.ceil(samples / base_buffer_size) * base_buffer_size

        if not max_buffer_size:
            # Max buffer size about 100 MB
            max_buffer_size = (100 * 1024**2) / 8

        if max_buffer_size > 0:
            max_buffer_size = math.ceil(max_buffer_size / base_buffer_size) * base_buffer_size
            if buffer_size > max_buffer_size:
                logger.warning('Required buffer size ({}) will be shrinked to max_buffer_size ({})!'.format(
                    buffer_size, max_buffer_size
                ))
                buffer_repeats = math.ceil(buffer_size / max_buffer_size)
                buffer_size = max_buffer_size

        logger.info('repeats: {}'.format(repeats))
        logger.info('samples: {} (time: {:.5f} s)'.format(samples, samples / self.device.sample_rate))
        if max_buffer_size > 0:
            logger.info('max_buffer_size (samples): {} (repeats: {:.2f}, time: {:.5f} s)'.format(
                max_buffer_size, max_buffer_size / bins, max_buffer_size / self.device.sample_rate
            ))
        else:
            logger.info('max_buffer_size (samples): UNLIMITED')
        logger.info('buffer_size (samples): {} (repeats: {:.2f}, time: {:.5f} s)'.format(
            buffer_size, buffer_size / bins, buffer_size / self.device.sample_rate
        ))
        logger.info('buffer_repeats: {}'.format(buffer_repeats))

        return (buffer_repeats, zeros(buffer_size, numpy.complex64))

    def setup(self, bins, repeats, base_buffer_size=0, max_buffer_size=0, fft_window='hann',
              fft_overlap=0.5, crop_factor=0, log_scale=True, remove_dc=False, detrend=None,
              lnb_lo=0, tune_delay=0, reset_stream=False, max_threads=0, max_queue_size=0):
        """Prepare samples buffer and start streaming samples from device"""
        if self.device.is_streaming:
            self.device.stop_stream()

        base_buffer = self.device.start_stream(buffer_size=base_buffer_size)
        self._bins = bins
        self._repeats = repeats
        self._base_buffer_size = len(base_buffer)
        self._max_buffer_size = max_buffer_size
        self._buffer_repeats, self._buffer = self.create_buffer(
            bins, repeats, self._base_buffer_size, self._max_buffer_size
        )
        self._tune_delay = tune_delay
        self._reset_stream = reset_stream
        self._psd = psd.PSD(bins, self.device.sample_rate, fft_window=fft_window, fft_overlap=fft_overlap,
                            crop_factor=crop_factor, log_scale=log_scale, remove_dc=remove_dc, detrend=detrend,
                            lnb_lo=lnb_lo, max_threads=max_threads, max_queue_size=max_queue_size)
        self._writer = writer.formats[self._output_format](self._output)

    def stop(self):
        """Stop streaming samples from device and delete samples buffer"""
        if not self.device.is_streaming:
            return

        self.device.stop_stream()
        self._writer.close()

        self._bins = None
        self._repeats = None
        self._base_buffer_size = None
        self._max_buffer_size = None
        self._buffer_repeats = None
        self._buffer = None
        self._tune_delay = None
        self._reset_stream = None
        self._psd = None
        self._writer = None

    def psd(self, freq):
        """Tune to specified center frequency and compute Power Spectral Density"""
        if not self.device.is_streaming:
            raise RuntimeError('Streaming is not initialized, you must run setup() first!')

        # Tune to new frequency in main thread
        logger.debug('  Frequency hop: {:.2f} Hz'.format(freq))
        t_freq = time.time()
        if self.device.freq != freq:
            # Deactivate streaming before tuning
            if self._reset_stream:
                self.device.device.deactivateStream(self.device.stream)

            # Actually tune to new center frequency
            self.device.freq = freq

            # Reactivate straming after tuning
            if self._reset_stream:
                self.device.device.activateStream(self.device.stream)

            # Delay reading samples after tuning
            if self._tune_delay:
                t_delay = time.time()
                while True:
                    self.device.read_stream()
                    t_delay_end = time.time()
                    if t_delay_end - t_delay >= self._tune_delay:
                        break
                logger.debug('    Tune delay: {:.3f} s'.format(t_delay_end - t_delay))
        else:
            logger.debug('    Same frequency as before, tuning skipped')
        psd_state = self._psd.set_center_freq(freq)
        t_freq_end = time.time()
        logger.debug('    Tune time: {:.3f} s'.format(t_freq_end - t_freq))

        for repeat in range(self._buffer_repeats):
            logger.debug('    Repeat: {}'.format(repeat + 1))
            # Read samples from SDR in main thread
            t_acq = time.time()
            acq_time_start = datetime.datetime.utcnow()
            self.device.read_stream_into_buffer(self._buffer)
            acq_time_stop = datetime.datetime.utcnow()
            t_acq_end = time.time()
            logger.debug('      Acquisition time: {:.3f} s'.format(t_acq_end - t_acq))

            # Start FFT computation in another thread
            self._psd.update_async(psd_state, numpy.copy(self._buffer))

            t_final = time.time()

            if _shutdown:
                break

        psd_future = self._psd.result_async(psd_state)
        logger.debug('    Total hop time: {:.3f} s'.format(t_final - t_freq))

        return (psd_future, acq_time_start, acq_time_stop)

    def sweep(self, min_freq, max_freq, bins, repeats, runs=0, time_limit=0, overlap=0,
              fft_window='hann', fft_overlap=0.5, crop=False, log_scale=True, remove_dc=False, detrend=None, lnb_lo=0,
              tune_delay=0, reset_stream=False, base_buffer_size=0, max_buffer_size=0, max_threads=0, max_queue_size=0):
        """Sweep spectrum using frequency hopping"""
        self.setup(
            bins, repeats, base_buffer_size, max_buffer_size,
            fft_window=fft_window, fft_overlap=fft_overlap, crop_factor=overlap if crop else 0,
            log_scale=log_scale, remove_dc=remove_dc, detrend=detrend, lnb_lo=lnb_lo, tune_delay=tune_delay,
            reset_stream=reset_stream, max_threads=max_threads, max_queue_size=max_queue_size
        )

        try:
            freq_list = self.freq_plan(min_freq - lnb_lo, max_freq - lnb_lo, bins, overlap)
            t_start = time.time()
            run = 0
            while not _shutdown and (runs == 0 or run < runs):
                run += 1
                t_run_start = time.time()
                logger.debug('Run: {}'.format(run))

                for freq in freq_list:
                    # Tune to new frequency, acquire samples and compute Power Spectral Density
                    psd_future, acq_time_start, acq_time_stop = self.psd(freq)

                    # Write PSD to stdout (in another thread)
                    self._writer.write_async(psd_future, acq_time_start, acq_time_stop,
                                             len(self._buffer) * self._buffer_repeats)

                    if _shutdown:
                        break

                # Write end of measurement marker (in another thread)
                write_next_future = self._writer.write_next_async()
                t_run = time.time()
                logger.debug('  Total run time: {:.3f} s'.format(t_run - t_run_start))

                # End measurement if time limit is exceeded
                if time_limit and (time.time() - t_start) >= time_limit:
                    logger.info('Time limit of {} s exceeded, completed {} runs'.format(time_limit, run))
                    break

            # Wait for last write to be finished
            write_next_future.result()

            # Debug thread pool queues
            logging.debug('Number of USB buffer overflow errors: {}'.format(self.device.buffer_overflow_count))
            logging.debug('PSD worker threads: {}'.format(self._psd._executor._max_workers))
            logging.debug('Max. PSD queue size: {} / {}'.format(self._psd._executor.max_queue_size_reached,
                                                                self._psd._executor.max_queue_size))
            logging.debug('Writer worker threads: {}'.format(self._writer._executor._max_workers))
            logging.debug('Max. Writer queue size: {} / {}'.format(self._writer._executor.max_queue_size_reached,
                                                                   self._writer._executor.max_queue_size))
        finally:
            # Shutdown SDR
            self.stop()
            t_stop = time.time()
            logger.info('Total time: {:.3f} s'.format(t_stop - t_start))


# smbus.py - cffi based python bindings for SMBus based on smbusmodule.c
# Copyright (C) 2013-2015 <david.schneider@bivab.de>
#
# smbusmodule.c - Python bindings for Linux SMBus access through i2c-dev
# Copyright (C) 2005-2007 Mark M. Hoffman <mhoffman@lightlink.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

"""This module defines an object type that allows SMBus transactions
on hosts running the Linux kernel.  The host kernel must have I2C
support, I2C device interface support, and a bus adapter driver.
All of these can be either built-in to the kernel, or loaded from
modules.

Because the I2C device interface is opened R/W, users of this
module usually must have root permissions."""

import os

from .util import validate
from .util import int2byte
from fcntl import ioctl

from ._smbus_cffi import ffi
from ._smbus_cffi import lib as SMBUS

MAXPATH = 16

class SMBus(object):
    """SMBus([bus]) -> SMBus
    Return a new SMBus object that is (optionally) connected to the
    specified I2C device interface.
    """

    _fd = -1
    _addr = -1
    _pec = 0
    # compat mode, enables some features that are not compatible with the
    # original smbusmodule.c
    _compat = False

    def __init__(self, bus=-1):
        if bus >= 0:
            self.open(bus)

    def close(self):
        """close()

        Disconnects the object from the bus.
        """
        os.close(self._fd)
        self._fd = -1
        self._addr = -1
        self._pec = 0

    def dealloc(self):
        self.close()

    def open(self, bus):
        """open(bus)

        Connects the object to the specified SMBus.
        """
        bus = int(bus)
        path = "/dev/i2c-%d" % (bus,)
        if len(path) >= MAXPATH:
                raise OverflowError("Bus number is invalid.")
        try:
            self._fd = os.open(path, os.O_RDWR, 0)
        except OSError as e:
            raise IOError(e.errno)

    def _set_addr(self, addr):
        """private helper method"""
        if self._addr != addr:
            ioctl(self._fd, SMBUS.I2C_SLAVE, addr)
            self._addr = addr

    @validate(addr=int)
    def write_quick(self, addr):
        """write_quick(addr)

        Perform SMBus Quick transaction.
        """
        self._set_addr(addr)
        if SMBUS.i2c_smbus_write_quick(self._fd, SMBUS.I2C_SMBUS_WRITE) != 0:
            raise IOError(ffi.errno)

    @validate(addr=int)
    def read_byte(self, addr):
        """read_byte(addr) -> result

        Perform SMBus Read Byte transaction.
        """
        self._set_addr(addr)
        result = SMBUS.i2c_smbus_read_byte(self._fd)
        if result == -1:
            raise IOError(ffi.errno)
        return result

    @validate(addr=int, val=int)
    def write_byte(self, addr, val):
        """write_byte(addr, val)

        Perform SMBus Write Byte transaction.
        """
        self._set_addr(addr)
        if SMBUS.i2c_smbus_write_byte(self._fd, ffi.cast("__u8", val)) == -1:
            raise IOError(ffi.errno)

    @validate(addr=int, cmd=int)
    def read_byte_data(self, addr, cmd):
        """read_byte_data(addr, cmd) -> result

        Perform SMBus Read Byte Data transaction.
        """
        self._set_addr(addr)
        res = SMBUS.i2c_smbus_read_byte_data(self._fd, ffi.cast("__u8", cmd))
        if res == -1:
            raise IOError(ffi.errno)
        return res

    @validate(addr=int, cmd=int, val=int)
    def write_byte_data(self, addr, cmd, val):
        """write_byte_data(addr, cmd, val)

        Perform SMBus Write Byte Data transaction.
        """
        self._set_addr(addr)
        if SMBUS.i2c_smbus_write_byte_data(self._fd,
                                           ffi.cast("__u8", cmd),
                                           ffi.cast("__u8", val)) == -1:
            raise IOError(ffi.errno)

    @validate(addr=int, cmd=int)
    def read_word_data(self, addr, cmd):
        """read_word_data(addr, cmd) -> result

        Perform SMBus Read Word Data transaction.
        """
        self._set_addr(addr)
        result = SMBUS.i2c_smbus_read_word_data(self._fd, ffi.cast("__u8", cmd))
        if result == -1:
            raise IOError(ffi.errno)
        return result

    @validate(addr=int, cmd=int, val=int)
    def write_word_data(self, addr, cmd, val):
        """write_word_data(addr, cmd, val)

        Perform SMBus Write Word Data transaction.
        """
        self._set_addr(addr)
        if SMBUS.i2c_smbus_write_word_data(self._fd,
                                           ffi.cast("__u8", cmd),
                                           ffi.cast("__u16", val)) == -1:
            raise IOError(ffi.errno)

    @validate(addr=int, cmd=int, val=int)
    def process_call(self, addr, cmd, val):
        """process_call(addr, cmd, val)

        Perform SMBus Process Call transaction.

        Note: although i2c_smbus_process_call returns a value, according to
        smbusmodule.c this method does not return a value by default.

        Set _compat = False on the SMBus instance to get a return value.
        """
        self._set_addr(addr)
        ret = SMBUS.i2c_smbus_process_call(self._fd,
                                           ffi.cast("__u8", cmd),
                                           ffi.cast("__u16", val))
        if ret == -1:
            raise IOError(ffi.errno)
        if self._compat:
            return ret

    @validate(addr=int, cmd=int)
    def read_block_data(self, addr, cmd):
        """read_block_data(addr, cmd) -> results

        Perform SMBus Read Block Data transaction.
        """
        # XXX untested, the raspberry pi i2c driver does not support this
        # command
        self._set_addr(addr)
        data = ffi.new("union i2c_smbus_data *")
        if SMBUS.i2c_smbus_access(self._fd,
                                  int2byte(SMBUS.I2C_SMBUS_READ),
                                  ffi.cast("__u8", cmd),
                                  SMBUS.I2C_SMBUS_BLOCK_DATA,
                                  data):
            raise IOError(ffi.errno)
        return smbus_data_to_list(data)

    @validate(addr=int, cmd=int, vals=list)
    def write_block_data(self, addr, cmd, vals):
        """write_block_data(addr, cmd, vals)

        Perform SMBus Write Block Data transaction.
        """
        self._set_addr(addr)
        data = ffi.new("union i2c_smbus_data *")
        list_to_smbus_data(data, vals)
        if SMBUS.i2c_smbus_access(self._fd,
                                  int2byte(SMBUS.I2C_SMBUS_WRITE),
                                  ffi.cast("__u8", cmd),
                                  SMBUS.I2C_SMBUS_BLOCK_DATA,
                                  data):
            raise IOError(ffi.errno)

    @validate(addr=int, cmd=int, vals=list)
    def block_process_call(self, addr, cmd, vals):
        """block_process_call(addr, cmd, vals) -> results

        Perform SMBus Block Process Call transaction.
        """
        self._set_addr(addr)
        data = ffi.new("union i2c_smbus_data *")
        list_to_smbus_data(data, vals)
        if SMBUS.i2c_smbus_access(self._fd, SMBUS.I2C_SMBUS_WRITE,
                                  ffi.cast("__u8", cmd),
                                  SMBUS.I2C_SMBUS_BLOCK_PROC_CALL,
                                  data):
            raise IOError(ffi.errno)
        return smbus_data_to_list(data)

    @validate(addr=int, cmd=int, len=int)
    def read_i2c_block_data(self, addr, cmd, len=32):
        """read_i2c_block_data(addr, cmd, len=32) -> results

        Perform I2C Block Read transaction.
        """
        self._set_addr(addr)
        data = ffi.new("union i2c_smbus_data *")
        data.block[0] = len
        if len == 32:
            arg = SMBUS.I2C_SMBUS_I2C_BLOCK_BROKEN
        else:
            arg = SMBUS.I2C_SMBUS_I2C_BLOCK_DATA
        if SMBUS.i2c_smbus_access(self._fd,
                                  int2byte(SMBUS.I2C_SMBUS_READ),
                                  ffi.cast("__u8", cmd),
                                  arg, data):
            raise IOError(ffi.errno)
        return smbus_data_to_list(data)

    @validate(addr=int, cmd=int, vals=list)
    def write_i2c_block_data(self, addr, cmd, vals):
        """write_i2c_block_data(addr, cmd, vals)

        Perform I2C Block Write transaction.
        """
        self._set_addr(addr)
        data = ffi.new("union i2c_smbus_data *")
        list_to_smbus_data(data, vals)
        if SMBUS.i2c_smbus_access(self._fd,
                                  int2byte(SMBUS.I2C_SMBUS_WRITE),
                                  ffi.cast("__u8", cmd),
                                  SMBUS.I2C_SMBUS_I2C_BLOCK_BROKEN,
                                  data):
            raise IOError(ffi.errno)

    @property
    def pec(self):
        return self._pec

    @pec.setter
    def pec(self, value):
        """True if Packet Error Codes (PEC) are enabled"""
        pec = bool(value)
        if pec != self._pec:
            if ioctl(self._fd, SMBUS.I2C_PEC, pec):
                raise IOError(ffi.errno)
            self._pec = pec

def smbus_data_to_list(data):
    block = data.block
    return [block[i + 1] for i in range(block[0])]

def list_to_smbus_data(data, vals):
    block_max = SMBUS.I2C_SMBUS_BLOCK_MAX
    if len(vals) > block_max or len(vals) == 0:
        raise OverflowError("Third argument must be a list of at least one, "
                            "but not more than %d integers" % block_max)
    data.block[0] = len(vals)
    for i, val in enumerate(vals):
        data.block[i + 1] = val


from setuptools import setup#, find_packages, Extension
import distutils.command.build as _build
import setuptools.command.install as _install

import sys
import os
import os.path as op
import distutils.spawn as ds
import distutils.dir_util as dd
import posixpath

def run_cmake(arg=""):
    """
    Forcing to run cmake
    """
    if ds.find_executable('cmake') is None:
        print "CMake  is required to build zql"
        print "Please install cmake version >= 2.8 and re-run setup"
        sys.exit(-1)

    print "Configuring zql build with CMake.... "
    cmake_args = arg
    try:
        build_dir = op.join(op.split(__file__)[0], 'build')
        dd.mkpath(build_dir)
        os.chdir("build")
        ds.spawn(['cmake', '..'] + cmake_args.split())
        ds.spawn(['make', 'clean'])
        ds.spawn(['make'])
        os.chdir("..")
    except ds.DistutilsExecError:
        print "Error while running cmake"
        print "run 'setup.py build --help' for build options"
        print "You may also try editing the settings in CMakeLists.txt file and re-running setup"
        sys.exit(-1)

class build(_build.build):
    def run(self):
        run_cmake()
        # Now populate the extension module  attribute.
        #self.distribution.ext_modules = get_ext_modules()
        _build.build.run(self)

class install(_install.install):
    def run(self):
        if not posixpath.exists("src/zq.so"):
            run_cmake()
        ds.spawn(['make', 'install'])
        #self.distribution.ext_modules = get_ext_modules()
        self.do_egg_install()

with open('README.txt') as file:
    clips6_long_desc = file.read()

setup(
    name = "zq",
    version = '0.6',
    description = 'ZQL - Zabbix Query Language',
    install_requires = ["cython", "msgpack-python", "simplejson", "hy", "pyfiglet",
                "gevent", "json", "termcolor", "humanfriendly", "ipaddr", "pyfscache",
                "Cheetah", "dateparser", "pygithub",
    ],
    requires = [],
    include_package_data = True,
    url = 'https://github.com/vulogov/zq/',
    author='Vladimir Ulogov',
    author_email = 'vladimir.ulogov@me.com',
    maintainer_email = 'vladimir.ulogov@me.com',
    license = "GNU GPL Versin 3",
    long_description = clips6_long_desc,
    keywords = "zql, monitoring, zabbix",
    platforms = ['GNU/Linux','Unix','Mac OS-X'],
    classifiers = [
            'Development Status :: 3 - Alpha',
            'Intended Audience :: Developers',
            'Intended Audience :: Information Technology',
            'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
            'Operating System :: OS Independent',
            'Programming Language :: Python :: 2',
            'Programming Language :: Python :: 2.6',
            'Programming Language :: Python :: 2.7',
            'Topic :: Software Development',
            'Topic :: Software Development :: Libraries',
            'Topic :: Software Development :: Libraries :: Python Modules',
            'Topic :: Software Development :: Libraries :: Python Modules',
            'Topic :: System :: Monitoring',
            'Topic :: System :: Networking :: Monitoring',
            'Topic :: System :: Systems Administration',
            'Environment :: Console',
            'Environment :: Console :: Curses'
    ],
    # ext_modules is not present here. This will be generated through CMake via the
    # build or install commands
    cmdclass={'install':install,'build': build},
    zip_safe=False,
    packages = ['zq'],
    package_data = {
        'zq': ['zq.so', '*.pyx', '*.pyi']
    }
)


"""
Filters used by GrandFatherSon to decide which datetimes to keep.
"""
from __future__ import division

import calendar
from datetime import datetime, time, timedelta, tzinfo

# As gleefully stolen from the python datetime docs
class UTC(tzinfo):
    """UTC"""
    ZERO = timedelta(0)

    def utcoffset(self, dt):
        return self.ZERO

    def tzname(self, dt):
        return "UTC"

    def dst(self, dt):
        return self.ZERO

class Filter(object):
    """Base class."""

    @classmethod
    def mask(cls, dt, **options):
        """
        Return a datetime with the same value as ``dt``, keeping only
        significant values.
        """
        raise NotImplemented

    @classmethod
    def start(cls, now, number, **options):
        """
        Return the starting datetime: ``number`` of units before ``now``.
        """
        return (cls.mask(now, **options) -
                timedelta(**{cls.__name__.lower(): number - 1}))

    @classmethod
    def filter(cls, datetimes, number, now=None, **options):
        """Return a set of datetimes, after filtering ``datetimes``.

        The result will be the ``datetimes`` which are ``number`` of
        units before ``now``, until ``now``, with approximately one
        unit between each of them.  The first datetime for any unit is
        kept, later duplicates are removed.

        If there are ``datetimes`` after ``now``, they will be
        returned unfiltered.
        """
        if not isinstance(number, int) or number < 0:
            raise ValueError('Invalid number: %s' % number)

        datetimes = tuple(datetimes)

        # Sample the first datetime to see if it is timezone-aware
        tzinfo = None
        if datetimes and datetimes[0].tzinfo is not None:
            tzinfo = UTC()

        if now is None:
            now = datetime.now(tzinfo)

        if not hasattr(now, 'second'):
            # now looks like a date, so convert it into a datetime
            now = datetime.combine(now, time(23, 59, 59, 999999, tzinfo=tzinfo))

        # Always keep datetimes from the future
        future = set(dt for dt in datetimes if dt > now)

        if number == 0:
            return future

        # Don't consider datetimes from before the start
        start = cls.start(now, number, **options)
        valid = (dt for dt in datetimes if start <= dt <= now)

        # Deduplicate datetimes with the same mask() value by keeping
        # the oldest.
        kept = {}
        for dt in sorted(valid):
            kept.setdefault(cls.mask(dt, **options), dt)

        return set(kept.values()) | future

class Seconds(Filter):
    @classmethod
    def mask(cls, dt, **options):
        """
        Return a datetime with the same value as ``dt``, to a
        resolution of seconds.
        """
        return dt.replace(microsecond=0)

class Minutes(Filter):
    @classmethod
    def mask(cls, dt, **options):
        """
        Return a datetime with the same value as ``dt``, to a
        resolution of minutes.
        """
        return dt.replace(second=0, microsecond=0)

class Hours(Filter):
    @classmethod
    def mask(cls, dt, **options):
        """
        Return a datetime with the same value as ``dt``, to a
        resolution of hours.
        """
        return dt.replace(minute=0, second=0, microsecond=0)

class Days(Filter):
    @classmethod
    def mask(cls, dt, **options):
        """
        Return a datetime with the same value as ``dt``, to a
        resolution of days.
        """
        return dt.replace(hour=0, minute=0, second=0, microsecond=0)

class Weeks(Filter):
    DAYS_IN_WEEK = 7

    @classmethod
    def start(cls, now, number, firstweekday=calendar.SATURDAY, **options):
        """
        Return the starting datetime: ``number`` of weeks before ``now``.

        ``firstweekday`` determines when the week starts. It defaults
        to Saturday.
        """
        week = cls.mask(now, firstweekday=firstweekday, **options)
        days = (number - 1) * cls.DAYS_IN_WEEK
        return week - timedelta(days=days)

    @classmethod
    def mask(cls, dt, firstweekday=calendar.SATURDAY, **options):
        """
        Return a datetime with the same value as ``dt``, to a
        resolution of weeks.

        ``firstweekday`` determines when the week starts. It defaults
        to Saturday.
        """
        correction = (dt.weekday() - firstweekday) % cls.DAYS_IN_WEEK
        week = dt - timedelta(days=correction)
        return week.replace(hour=0, minute=0, second=0, microsecond=0)

class Months(Filter):
    MONTHS_IN_YEAR = 12

    @classmethod
    def start(cls, now, number, **options):
        """
        Return the starting datetime: ``number`` of months before ``now``.
        """
        year = now.year
        month = now.month - number + 1
        # Handle negative months
        if month < 0:
            year = year + (month // cls.MONTHS_IN_YEAR)
            month = month % cls.MONTHS_IN_YEAR
        # Handle December
        if month == 0:
            year = year - 1
            month = 12
        return cls.mask(now, **options).replace(year=year, month=month)

    @classmethod
    def mask(cls, dt, **options):
        """
        Return a datetime with the same value as ``dt``, to a
        resolution of months.
        """
        return dt.replace(day=1,
                          hour=0, minute=0, second=0, microsecond=0)

class Years(Filter):
    @classmethod
    def start(cls, now, number, **options):
        """
        Return the starting datetime: ``number`` of years before ``now``.
        """
        return cls.mask(now).replace(year=(now.year - number + 1))

    @classmethod
    def mask(cls, dt, **options):
        """
        Return a datetime with the same value as ``dt``, to a
        resolution of years.
        """
        return dt.replace(month=1, day=1,
                          hour=0, minute=0, second=0, microsecond=0)


"""
GrandFatherSon is a backup rotation calculator that implements the
`grandfather-father-son rotation scheme
<http://en.wikipedia.org/wiki/Backup_rotation_scheme#Grandfather-father-son_backup>`_.

This is usually done by keeping a certain number of daily, weekly, and
monthly backups. Older backups should be removed to reduce the amount
of space used.

Usage
-----

This module expects either ``datetime.date`` or ``datetime.datetime``
objects as inputs. As an example, let's assume you have daily backups
for the all of year 1999 that need rotating::

    >>> import datetime
    >>> start_date = datetime.date(1999, 1, 1)
    >>> end_date = datetime.date(1999, 12, 31)
    >>> backups = [start_date + datetime.timedelta(days=i)
    ...            for i in range((end_date - start_date).days + 1)]
    >>> backups
    [datetime.date(1999, 1, 1),
     datetime.date(1999, 1, 2),
     datetime.date(1999, 1, 3),
     ...
     datetime.date(1999, 12, 30),
     datetime.date(1999, 12, 31)]

Let's say that full backups are taken every Saturday, with incremental
backups done daily. A week, or 7 days, of incremental backups should
be kept. A months, or 4 weeks, of full backups are kept. In addition,
for three months, the first full backup is kept for each month, with
the others discarded.

It's the last day of the year and you want to figure out which backups
need to be pruned::

    >>> now = datetime.date(1999, 12, 31)

To see which files will be preserved, use the ``dates_to_keep``
function::

    >>> from grandfatherson import dates_to_keep, SATURDAY
    >>> sorted(dates_to_keep(backups, days=7, weeks=4, months=3,
    ...                      firstweekday=SATURDAY, now=now))
    [datetime.date(1999, 10, 1),
     datetime.date(1999, 11, 1),
     datetime.date(1999, 12, 1),
     datetime.date(1999, 12, 4),
     datetime.date(1999, 12, 11),
     datetime.date(1999, 12, 18),
     datetime.date(1999, 12, 25),
     datetime.date(1999, 12, 26),
     datetime.date(1999, 12, 27),
     datetime.date(1999, 12, 28),
     datetime.date(1999, 12, 29),
     datetime.date(1999, 12, 30),
     datetime.date(1999, 12, 31)]

If you leave off the ``now`` argument, it will default to using
``datetime.datetime.now()``.

To see which files should be deleted, use the ``dates_to_delete``
function::

    >>> from grandfatherson import dates_to_delete, SATURDAY
    >>> sorted(dates_to_delete(backups, days=7, weeks=4, months=3,
    ...                        firstweekday=SATURDAY, now=now))
    [datetime.date(1999, 1, 1),
     ...
     datetime.date(1999, 9, 30),
     datetime.date(1999, 10, 2),
     ...
     datetime.date(1999, 10, 31),
     datetime.date(1999, 11, 2),
     ...
     datetime.date(1999, 11, 30),
     datetime.date(1999, 12, 2),
     datetime.date(1999, 12, 3),
     datetime.date(1999, 12, 5),
     ...
     datetime.date(1999, 12, 10),
     datetime.date(1999, 12, 12),
     ...
     datetime.date(1999, 12, 17),
     datetime.date(1999, 12, 19),
     ...
     datetime.date(1999, 12, 24)]

Finally, if you need to rotate backups that have timestamps in
``datetime`` format, you can use the corresponding ``to_keep`` and
``to_delete`` functions::

    >>> now = datetime.datetime(1999, 12, 31, 23, 59, 59)
    >>> start_datetime = datetime.datetime(1999, 12, 31, 0, 0, 0)
    >>> end_datetime = datetime.datetime(1999, 12, 31, 23, 59, 59)
    >>> backups = [start_datetime + datetime.timedelta(seconds=i)
    ...            for i
    ...            in range((end_datetime - start_datetime).seconds + 1)]
    >>> backups
    [datetime.datetime(1999, 12, 31, 0, 0),
     datetime.datetime(1999, 12, 31, 0, 0, 1),
     datetime.datetime(1999, 12, 31, 0, 0, 2),
     ...
     datetime.datetime(1999, 12, 31, 23, 59, 58),
     datetime.datetime(1999, 12, 31, 23, 59, 59)]

    >>> from grandfatherson import to_keep
    >>> sorted(to_keep(backups, hours=2, minutes=10, seconds=10, now=now))
    [datetime.datetime(1999, 12, 31, 22, 0),
     datetime.datetime(1999, 12, 31, 23, 0),
     datetime.datetime(1999, 12, 31, 23, 50),
     ...
     datetime.datetime(1999, 12, 31, 23, 59),
     datetime.datetime(1999, 12, 31, 23, 59, 50),
     ...
     datetime.datetime(1999, 12, 31, 23, 59, 59)]

    >>> from grandfatherson import to_delete
    >>> sorted(to_delete(backups, hours=2, minutes=10, seconds=10, now=now))
    [datetime.datetime(1999, 12, 31, 0, 0),
     ...
     datetime.datetime(1999, 12, 31, 21, 59, 59),
     datetime.datetime(1999, 12, 31, 22, 0, 1),
     ...
     datetime.datetime(1999, 12, 31, 22, 59, 59),
     datetime.datetime(1999, 12, 31, 23, 0, 1),
     ...
     datetime.datetime(1999, 12, 31, 23, 49, 59),
     datetime.datetime(1999, 12, 31, 23, 50, 1),
     ...
     datetime.datetime(1999, 12, 31, 23, 58, 59),
     datetime.datetime(1999, 12, 31, 23, 59, 1),
     ...
     datetime.datetime(1999, 12, 31, 23, 59, 49)]
"""

from calendar import (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY,
                      SUNDAY)
from datetime import datetime, time
import os

from grandfatherson import filters

__version__ = "1.3"

def to_keep(datetimes,
            years=0, months=0, weeks=0, days=0,
            hours=0, minutes=0, seconds=0,
            firstweekday=SATURDAY, now=None):
    """
    Return a set of datetimes that should be kept, out of ``datetimes``.

    Keeps up to ``years``, ``months``, ``weeks``, ``days``,
    ``hours``, ``minutes``, and ``seconds`` in the past.

    When keeping weeks, it prefers to keep ``firstweekday``, which
    defaults to Saturday.

    If ``now`` is None, it will base its calculations on
    ``datetime.datetime.now()``. Datetimes after this point will always be
    kept.
    """
    datetimes = set(datetimes)
    return (filters.Years.filter(datetimes, number=years, now=now) |
            filters.Months.filter(datetimes, number=months, now=now) |
            filters.Weeks.filter(datetimes, number=weeks,
                                 firstweekday=firstweekday, now=now) |
            filters.Days.filter(datetimes, number=days, now=now) |
            filters.Hours.filter(datetimes, number=hours, now=now) |
            filters.Minutes.filter(datetimes, number=minutes, now=now) |
            filters.Seconds.filter(datetimes, number=seconds, now=now))

def to_delete(datetimes,
              years=0, months=0, weeks=0, days=0,
              hours=0, minutes=0, seconds=0,
              firstweekday=SATURDAY, now=None):
    """
    Return a set of datetimes that should be deleted, out of ``datetimes``.

    See ``to_keep`` for a description of arguments.
    """
    datetimes = set(datetimes)
    return datetimes - to_keep(datetimes,
                               years=years, months=months,
                               weeks=weeks, days=days,
                               hours=hours, minutes=minutes, seconds=seconds,
                               firstweekday=firstweekday, now=now)

def dates_to_keep(dates,
                  years=0, months=0, weeks=0, days=0, firstweekday=SATURDAY,
                  now=None):
    """
    Return a set of dates that should be kept, out of ``dates``.

    See ``to_keep`` for a description of arguments.
    """
    datetimes = to_keep((datetime.combine(d, time()) for d in dates),
                        years=years, months=months, weeks=weeks, days=days,
                        hours=0, minutes=0, seconds=0,
                        firstweekday=firstweekday, now=now)
    return set(dt.date() for dt in datetimes)

def dates_to_delete(dates,
                    years=0, months=0, weeks=0, days=0, firstweekday=SATURDAY,
                    now=None):
    """
    Return a set of date that should be deleted, out of ``dates``.

    See ``to_keep`` for a description of arguments.
    """
    dates = set(dates)
    return dates - dates_to_keep(dates,
                                 years=years, months=months,
                                 weeks=weeks, days=days,
                                 firstweekday=firstweekday, now=now)


import os
import sys
from .core import (
    get_bit_mask,
    get_bit_num,
)
from .spi import SPIDevice
import pifacecommon.interrupts

# Python 2 support
PY3 = sys.version_info[0] >= 3

WRITE_CMD = 0
READ_CMD = 1

# Register addresses
IODIRA = 0x0  # I/O direction A
IODIRB = 0x1  # I/O direction B
IPOLA = 0x2  # I/O polarity A
IPOLB = 0x3  # I/O polarity B
GPINTENA = 0x4  # interupt enable A
GPINTENB = 0x5  # interupt enable B
DEFVALA = 0x6  # register default value A (interupts)
DEFVALB = 0x7  # register default value B (interupts)
INTCONA = 0x8  # interupt control A
INTCONB = 0x9  # interupt control B
IOCON = 0xA  # I/O config (also 0xB)
GPPUA = 0xC  # port A pullups
GPPUB = 0xD  # port B pullups
INTFA = 0xE  # interupt flag A (where the interupt came from)
INTFB = 0xF  # interupt flag B
INTCAPA = 0x10  # interupt capture A (value at interupt is saved here)
INTCAPB = 0x11  # interupt capture B
GPIOA = 0x12  # port A
GPIOB = 0x13  # port B
OLATA = 0x14  # output latch A
OLATB = 0x15  # output latch B

# I/O config
BANK_OFF = 0x00  # addressing mode
BANK_ON = 0x80
INT_MIRROR_ON = 0x40  # interupt mirror (INTa|INTb)
INT_MIRROR_OFF = 0x00
SEQOP_OFF = 0x20  # incrementing address pointer
SEQOP_ON = 0x00
DISSLW_ON = 0x10  # slew rate
DISSLW_OFF = 0x00
HAEN_ON = 0x08  # hardware addressing
HAEN_OFF = 0x00
ODR_ON = 0x04  # open drain for interupts
ODR_OFF = 0x00
INTPOL_HIGH = 0x02  # interupt polarity
INTPOL_LOW = 0x00

LOWER_NIBBLE, UPPER_NIBBLE = range(2)

class MCP23S17(SPIDevice):
    """Microchip's MCP23S17: A 16-Bit I/O Expander with Serial Interface.

    :attribute: iodira/iodirb -- Controls the direction of the data I/O.
    :attribute: ipola/ipolb --This register allows the user to configure
                              the polarity on the corresponding GPIO port
                              bits.
    :attribute: gpintena/gpintenb -- The GPINTEN register controls the
                                     interrupt-onchange feature for each
                                     pin.
    :attribute: defvala/defvalb --The default comparison value is
                                  configured in the DEFVAL register.
    :attribute: intcona/intconb --The INTCON register controls how the
                                  associated pin value is compared for
                                  the interrupt-on-change feature.
    :attribute: iocon --The IOCON register contains several bits for
                        configuring the device.
    :attribute: gppua/gppub --The GPPU register controls the pull-up
                              resistors for the port pins.
    :attribute: intfa/intfb --The INTF register reflects the interrupt
                              condition on the port pins of any pin that
                              is enabled for interrupts via the GPINTEN
                              register.
    :attribute: intcapa/intcapb -- The INTCAP register captures the GPIO
                                   port value at the time the interrupt
                                   occurred.
    :attribute: gpioa/gpiob -- The GPIO register reflects the value on
                               the port.
    :attribute: olata/olatb -- The OLAT register provides access to the
                               output latches.
    """
    def __init__(self, hardware_addr=0, bus=0, chip_select=0, speed_hz=100000):
        super(MCP23S17, self).__init__(bus, chip_select, speed_hz=speed_hz)
        self.hardware_addr = hardware_addr

        self.iodira = MCP23S17Register(IODIRA, self)
        self.iodirb = MCP23S17Register(IODIRB, self)
        self.ipola = MCP23S17Register(IPOLA, self)
        self.ipolb = MCP23S17Register(IPOLB, self)
        self.gpintena = MCP23S17Register(GPINTENA, self)
        self.gpintenb = MCP23S17Register(GPINTENB, self)
        self.defvala = MCP23S17Register(DEFVALA, self)
        self.defvalb = MCP23S17Register(DEFVALB, self)
        self.intcona = MCP23S17Register(INTCONA, self)
        self.intconb = MCP23S17Register(INTCONB, self)
        self.iocon = MCP23S17Register(IOCON, self)
        self.gppua = MCP23S17Register(GPPUA, self)
        self.gppub = MCP23S17Register(GPPUB, self)
        self.intfa = MCP23S17Register(INTFA, self)
        self.intfb = MCP23S17Register(INTFB, self)
        self.intcapa = MCP23S17Register(INTCAPA, self)
        self.intcapb = MCP23S17Register(INTCAPB, self)
        self.gpioa = MCP23S17Register(GPIOA, self)
        self.gpiob = MCP23S17Register(GPIOB, self)
        self.olata = MCP23S17Register(OLATA, self)
        self.olatb = MCP23S17Register(OLATB, self)

    def _get_spi_control_byte(self, read_write_cmd):
        """Returns an SPI control byte.

        The MCP23S17 is a slave SPI device. The slave address contains
        four fixed bits and three user-defined hardware address bits
        (if enabled via IOCON.HAEN) (pins A2, A1 and A0) with the
        read/write bit filling out the control byte::

            +--------------------+
            |0|1|0|0|A2|A1|A0|R/W|
            +--------------------+
             7 6 5 4 3  2  1   0

        :param read_write_cmd: Read or write command.
        :type read_write_cmd: int
        """
        # board_addr_pattern = (self.hardware_addr & 0b111) << 1
        board_addr_pattern = (self.hardware_addr << 1) & 0xE
        rw_cmd_pattern = read_write_cmd & 1  # make sure it's just 1 bit long
        return 0x40 | board_addr_pattern | rw_cmd_pattern

    def read(self, address):
        """Returns the value of the address specified.

        :param address: The address to read from.
        :type address: int
        """
        return self._pyver_read(address)

    def _py3read(self, address):
        ctrl_byte = self._get_spi_control_byte(READ_CMD)
        op, addr, data = self.spisend(bytes((ctrl_byte, address, 0)))
        return data

    def _py2read(self, address):
        ctrl_byte = self._get_spi_control_byte(READ_CMD)
        op, addr, data = self.spisend(chr(ctrl_byte)+chr(address)+chr(0))
        return ord(data)

    def write(self, data, address):
        """Writes data to the address specified.

        :param data: The data to write.
        :type data: int
        :param address: The address to write to.
        :type address: int
        """
        self._pyver_write(data, address)

    def _py3write(self, data, address):
        ctrl_byte = self._get_spi_control_byte(WRITE_CMD)
        self.spisend(bytes((ctrl_byte, address, data)))

    def _py2write(self, data, address):
        ctrl_byte = self._get_spi_control_byte(WRITE_CMD)
        self.spisend(chr(ctrl_byte)+chr(address)+chr(data))

    # Python 2 support
    _pyver_read = _py3read if PY3 else _py2read
    _pyver_write = _py3write if PY3 else _py2write

    def read_bit(self, bit_num, address):
        """Returns the bit specified from the address.

        :param bit_num: The bit number to read from.
        :type bit_num: int
        :param address: The address to read from.
        :type address: int
        :returns: int -- the bit value from the address
        """
        value = self.read(address)
        bit_mask = get_bit_mask(bit_num)
        return 1 if value & bit_mask else 0

    def write_bit(self, value, bit_num, address):
        """Writes the value given to the bit in the address specified.

        :param value: The value to write.
        :type value: int
        :param bit_num: The bit number to write to.
        :type bit_num: int
        :param address: The address to write to.
        :type address: int
        """
        bit_mask = get_bit_mask(bit_num)
        old_byte = self.read(address)
         # generate the new byte
        if value:
            new_byte = old_byte | bit_mask
        else:
            new_byte = old_byte & ~bit_mask
        self.write(new_byte, address)

    def clear_interrupts(self, port):
        """Clears the interrupt flags by 'read'ing the capture register."""
        self.read(INTCAPA if port == GPIOA else INTCAPB)

class MCP23S17RegisterBase(object):
    """Base class for objects on an 8-bit register inside an MCP23S17."""
    def __init__(self, address, chip):
        self.address = address
        self.chip = chip

class MCP23S17Register(MCP23S17RegisterBase):
    """An 8-bit register inside an MCP23S17."""
    def __init__(self, address, chip):
        super(MCP23S17Register, self).__init__(address, chip)
        self.lower_nibble = MCP23S17RegisterNibble(LOWER_NIBBLE, self.address,
                                                   self.chip)
        self.upper_nibble = MCP23S17RegisterNibble(UPPER_NIBBLE, self.address,
                                                   self.chip)
        self.bits = [MCP23S17RegisterBit(i, self.address, self.chip)
                     for i in range(8)]

    @property
    def value(self):
        return self.chip.read(self.address)

    @value.setter
    def value(self, v):
        self.chip.write(v, self.address)

    def all_high(self):
        self.value = 0xFF

    def all_low(self):
        self.value = 0x00

    all_on = all_high
    all_off = all_low

    def toggle(self):
        self.value = 0xFF ^ self.value

class MCP23S17RegisterNeg(MCP23S17Register):
    """An negated 8-bit register inside an MCP23S17."""
    def __init__(self, address, chip):
        super(MCP23S17RegisterNeg, self).__init__(address, chip)
        self.lower_nibble = MCP23S17RegisterNibbleNeg(LOWER_NIBBLE,
                                                      self.address,
                                                      self.chip)
        self.upper_nibble = MCP23S17RegisterNibbleNeg(UPPER_NIBBLE,
                                                      self.address,
                                                      self.chip)
        self.bits = [MCP23S17RegisterBitNeg(i, self.address, self.chip)
                     for i in range(8)]

    @property
    def value(self):
        return 0xFF ^ self.chip.read(self.address)

    @value.setter
    def value(self, v):
        self.chip.write(0xFF ^ v, self.address)

class MCP23S17RegisterNibble(MCP23S17RegisterBase):
    """An 4-bit nibble inside a register inside an MCP23S17."""
    def __init__(self, nibble, address, chip):
        super(MCP23S17RegisterNibble, self).__init__(address, chip)
        self.nibble = nibble
        range_start = 0 if self.nibble == LOWER_NIBBLE else 4
        range_end = 4 if self.nibble == LOWER_NIBBLE else 8
        self.bits = [MCP23S17RegisterBit(i, self.address, self.chip)
                     for i in range(range_start, range_end)]

    @property
    def value(self):
        if self.nibble == LOWER_NIBBLE:
            return self.chip.read(self.address) & 0xF
        elif self.nibble == UPPER_NIBBLE:
            return (self.chip.read(self.address) & 0xF0) >> 4

    @value.setter
    def value(self, v):
        register_value = self.chip.read(self.address)
        if self.nibble == LOWER_NIBBLE:
            register_value &= 0xF0  # clear
            register_value ^= (v & 0x0F)  # set
        elif self.nibble == UPPER_NIBBLE:
            register_value &= 0x0F  # clear
            register_value ^= ((v << 4) & 0xF0)  # set
        self.chip.write(register_value, self.address)

    def all_high(self):
        self.value = 0xF

    def all_low(self):
        self.value = 0x0

    all_on = all_high
    all_off = all_low

    def toggle(self):
        self.value = 0xF ^ self.value

class MCP23S17RegisterNibbleNeg(MCP23S17RegisterNibble):
    """A negated 4-bit nibble inside a register inside an MCP23S17."""
    def __init__(self, nibble, address, chip):
        super(MCP23S17RegisterNibbleNeg, self).__init__(nibble, address, chip)
        self.nibble = nibble
        range_start = 0 if self.nibble == LOWER_NIBBLE else 4
        range_end = 4 if self.nibble == LOWER_NIBBLE else 8
        self.bits = [MCP23S17RegisterBitNeg(i, self.address, self.chip)
                     for i in range(range_start, range_end)]

    @property
    def value(self):
        if self.nibble == LOWER_NIBBLE:
            v = self.chip.read(self.address) & 0xF
        elif self.nibble == UPPER_NIBBLE:
            v = (self.chip.read(self.address) & 0xF0) >> 4
        return 0xF ^ v

    @value.setter
    def value(self, v):
        register_value = self.chip.read(self.address)
        if self.nibble == LOWER_NIBBLE:
            register_value &= 0xF0  # clear
            register_value ^= (v & 0x0F ^ 0x0F)  # set
        elif self.nibble == UPPER_NIBBLE:
            register_value &= 0x0F  # clear
            register_value ^= ((v << 4) & 0xF0 ^ 0xF0)  # set
        self.chip.write(register_value, self.address)

class MCP23S17RegisterBit(MCP23S17RegisterBase):
    """A bit inside register inside an MCP23S17."""
    def __init__(self, bit_num, address, chip):
        super(MCP23S17RegisterBit, self).__init__(address, chip)
        self.bit_num = bit_num

    @property
    def value(self):
        return self.chip.read_bit(self.bit_num, self.address)

    @value.setter
    def value(self, v):
        self.chip.write_bit(v, self.bit_num, self.address)

    def set_high(self):
        self.value = 1

    def set_low(self):
        self.value = 0

    turn_on = set_high
    turn_off = set_low

    def toggle(self):
        self.value = 1 ^ self.value

class MCP23S17RegisterBitNeg(MCP23S17RegisterBit):
    """A negated bit inside register inside an MCP23S17."""
    def __init__(self, bit_num, address, chip):
        super(MCP23S17RegisterBitNeg, self).__init__(bit_num, address, chip)

    @property
    def value(self):
        return 1 ^ self.chip.read_bit(self.bit_num, self.address)

    @value.setter
    def value(self, v):
        self.chip.write_bit(v ^ 1, self.bit_num, self.address)


def get_bit_mask(bit_num):
    """Returns as bit mask with bit_num set.

    :param bit_num: The bit number.
    :type bit_num: int
    :returns: int -- the bit mask
    :raises: RangeError

    >>> bin(pifacecommon.core.get_bit_mask(0))
    1
    >>> pifacecommon.core.get_bit_mask(1)
    2
    >>> bin(pifacecommon.core.get_bit_mask(3))
    '0b1000'
    """
    return 1 << (bit_num)

def get_bit_num(bit_pattern):
    """Returns the lowest bit num from a given bit pattern. Returns None if no
    bits set.

    :param bit_pattern: The bit pattern.
    :type bit_pattern: int
    :returns: int -- the bit number
    :returns: None -- no bits set

    >>> pifacecommon.core.get_bit_num(0)
    None
    >>> pifacecommon.core.get_bit_num(0b1)
    0
    >>> pifacecommon.core.get_bit_num(0b11000)
    3
    """
    if bit_pattern == 0:
        return None

    bit_num = 0  # assume bit 0
    while (bit_pattern & 1) == 0:
        bit_pattern = bit_pattern >> 1
        bit_num += 1
        if bit_num > 7:
            bit_num = 0
            break

    return bit_num

def sleep_microseconds(microseconds):
    """Sleeps for the given number of microseconds.

    :param microseconds: Number of microseconds to sleep for.
    :type microseconds: int
    """
    # divide microseconds by 1 million for seconds
    seconds = microseconds / float(1000000)
    time.sleep(seconds)


import threading
import multiprocessing
import select
import time
import errno
from .core import get_bit_num
import pifacecommon.mcp23s17

# interrupts
IODIR_FALLING_EDGE = IODIR_ON = 0
IODIR_RISING_EDGE = IODIR_OFF = 1
IODIR_BOTH = None
# IN_EVENT_DIR_OFF = INPUT_DIRECTION_OFF = 1
# IN_EVENT_DIR_BOTH = INPUT_DIRECTION_BOTH = None

GPIO_INTERRUPT_PIN = 25
GPIO_INTERRUPT_DEVICE = "/sys/class/gpio/gpio%d" % GPIO_INTERRUPT_PIN
GPIO_INTERRUPT_DEVICE_EDGE = '%s/edge' % GPIO_INTERRUPT_DEVICE
GPIO_INTERRUPT_DEVICE_VALUE = '%s/value' % GPIO_INTERRUPT_DEVICE
GPIO_EXPORT_FILE = "/sys/class/gpio/export"
GPIO_UNEXPORT_FILE = "/sys/class/gpio/unexport"

# max seconds to wait for file I/O (when enabling interrupts)
FILE_IO_TIMEOUT = 1

READY_FOR_EVENTS = "ready for events"

# deboucing
DEFAULT_SETTLE_TIME = 0.020  # 20ms

class Timeout(Exception):
    pass

class InterruptEvent(object):
    """An interrupt event containting the interrupt flag and capture register
    values, the chip object from which the interrupt occured and a timestamp.
    """
    def __init__(
            self, interrupt_flag, interrupt_capture, chip, timestamp):
        self.interrupt_flag = interrupt_flag
        self.interrupt_capture = interrupt_capture
        self.chip = chip
        self.timestamp = timestamp

    def __str__(self):
        s = "interrupt_flag:    {flag}\n" \
            "interrupt_capture: {capture}\n" \
            "pin_num:           {pin_num}\n" \
            "direction:         {direction}\n" \
            "chip:              {chip}\n" \
            "timestamp:         {timestamp}"
        return s.format(flag=bin(self.interrupt_flag),
                        capture=bin(self.interrupt_capture),
                        pin_num=self.pin_num,
                        direction=self.direction,
                        chip=self.chip,
                        timestamp=self.timestamp)

    @property
    def pin_num(self):
        return get_bit_num(self.interrupt_flag)

    @property
    def direction(self):
        return (self.interrupt_flag & self.interrupt_capture) >> self.pin_num

class FunctionMap(object):
    """Maps something to a callback function.
    (This is an abstract class, you must implement a SomethingFunctionMap).
    """
    def __init__(self, callback, settle_time=None):
        self.callback = callback
        self.settle_time = settle_time

class PinFunctionMap(FunctionMap):
    """Maps an IO pin and a direction to callback function."""
    def __init__(self, pin_num, direction, callback, settle_time):
        self.pin_num = pin_num
        self.direction = direction
        super(PinFunctionMap, self).__init__(callback, settle_time)

    def __str__(self):
        s = "Pin num:  {pin_num}\n" \
            "Direction:{direction}\n"
        return s.format(pin_num=self.pin_num,
                        direction=self.direction)

class EventQueue(object):
    """Stores events in a queue."""
    def __init__(self, pin_function_maps):
        super(EventQueue, self).__init__()
        self.last_event_time = [0]*8  # last event time on each pin
        self.pin_function_maps = pin_function_maps
        self.queue = multiprocessing.Queue()

    def add_event(self, event):
        """Adds events to the queue. Will ignore events that occur before the
        settle time for that pin/direction. Such events are assumed to be
        bouncing.
        """
        # print("Trying to add event:")
        # print(event)
        # find out the pin settle time
        for pin_function_map in self.pin_function_maps:
            if _event_matches_pin_function_map(event, pin_function_map):
            # if pin_function_map.pin_num == event.pin_num and (
            #         pin_function_map.direction == event.direction or
            #         pin_function_map.direction == IODIR_BOTH):
                pin_settle_time = pin_function_map.settle_time
                # print("EventQueue: Found event in map.")
                break
        else:
            # Couldn't find event in map, don't bother adding it to the queue
            # print("EventQueue: Couldn't find event in map:")
            # for pin_function_map in self.pin_function_maps:
            #     print(pin_function_map)
            return

        threshold_time = self.last_event_time[event.pin_num] + pin_settle_time
        if event.timestamp > threshold_time:
            self.put(event)
            self.last_event_time[event.pin_num] = event.timestamp

    def put(self, thing):
        self.queue.put(thing)

    def get(self):
        return self.queue.get()

class PortEventListener(object):
    """Listens for port events and calls the registered functions.

    >>> def print_flag(event):
    ...     print(event.interrupt_flag)
    ...
    >>> port = pifacecommon.mcp23s17.GPIOA
    >>> listener = pifacecommon.interrupts.PortEventListener(port)
    >>> listener.register(0, pifacecommon.interrupts.IODIR_ON, print_flag)
    >>> listener.activate()
    """

    TERMINATE_SIGNAL = "astalavista"

    def __init__(self, port, chip, return_after_kbdint=True, daemon=False):
        self.port = port
        self.chip = chip
        self.pin_function_maps = list()
        self.event_queue = EventQueue(self.pin_function_maps)
        self.detector = multiprocessing.Process(
            target=watch_port_events,
            args=(
                self.port,
                self.chip,
                self.pin_function_maps,
                self.event_queue,
                return_after_kbdint))
        self.detector.daemon = daemon
        self.dispatcher = threading.Thread(
            target=handle_events,
            args=(
                self.pin_function_maps,
                self.event_queue,
                _event_matches_pin_function_map,
                PortEventListener.TERMINATE_SIGNAL))
        self.dispatcher.daemon = daemon

    def register(self, pin_num, direction, callback,
                 settle_time=DEFAULT_SETTLE_TIME):
        """Registers a pin number and direction to a callback function.

        :param pin_num: The pin pin number.
        :type pin_num: int
        :param direction: The event direction
            (use: IODIR_ON/IODIR_OFF/IODIR_BOTH)
        :type direction: int
        :param callback: The function to run when event is detected.
        :type callback: function
        :param settle_time: Time within which subsequent events are ignored.
        :type settle_time: int
        """
        self.pin_function_maps.append(
            PinFunctionMap(pin_num, direction, callback, settle_time))

    def deregister(self, pin_num=None, direction=None):
        """De-registers callback functions

        :param pin_num: The pin number. If None then all functions are de-registered
        :type pin_num: int
        :param direction: The event direction. If None then all functions for the
                          given pin are de-registered
        :type direction:int
        """
        to_delete = []
        for i, function_map in enumerate(self.pin_function_maps):
            if ( pin_num == None
                 or ( function_map.pin_num == pin_num
                      and ( direction == None
                            or function_map.direction == direction ) ) ):
                to_delete.append(i)
        for i in reversed(to_delete):
            del self.pin_function_maps[i]

    def activate(self):
        """When activated the :class:`PortEventListener` will run callbacks
        associated with pins/directions.
        """
        self.detector.start()
        self.dispatcher.start()

    def deactivate(self):
        """When deactivated the :class:`PortEventListener` will not run
        anything.
        """
        self.event_queue.put(self.TERMINATE_SIGNAL)
        self.dispatcher.join()
        self.detector.terminate()
        self.detector.join()

class GPIOInterruptDevice(object):
    """A device that interrupts using the GPIO pins."""
    def gpio_interrupts_enable(self):
        """Enables GPIO interrupts."""
        try:
            bring_gpio_interrupt_into_userspace()
            set_gpio_interrupt_edge()
        except Timeout as e:
            raise InterruptEnableException(
                "There was an error bringing gpio%d into userspace. %s" %
                (GPIO_INTERRUPT_PIN, e.message)
            )

    def gpio_interrupts_disable(self):
        """Disables gpio interrupts."""
        set_gpio_interrupt_edge('none')
        deactivate_gpio_interrupt()

def _event_matches_pin_function_map(event, pin_function_map):
    # print("pin num", event.pin_num, pin_function_map.pin_num)
    # print("direction", event.direction, pin_function_map.direction)
    pin_match = event.pin_num == pin_function_map.pin_num
    direction_match = pin_function_map.direction is None
    direction_match |= event.direction == pin_function_map.direction
    return pin_match and direction_match

def watch_port_events(port, chip, pin_function_maps, event_queue,
                      return_after_kbdint=False):
    """Waits for a port event. When a port event occurs it is placed onto the
    event queue.

    :param port: The port we are waiting for interrupts on (GPIOA/GPIOB).
    :type port: int
    :param chip: The chip we are waiting for interrupts on.
    :type chip: :class:`pifacecommon.mcp23s17.MCP23S17`
    :param pin_function_maps: A list of classes that have inheritted from
        :class:`FunctionMap`\ s describing what to do with events.
    :type pin_function_maps: list
    :param event_queue: A queue to put events on.
    :type event_queue: :py:class:`multiprocessing.Queue`
    """
    # set up epoll
    gpio25 = open(GPIO_INTERRUPT_DEVICE_VALUE, 'r')  # change to use 'with'?
    epoll = select.epoll()
    epoll.register(gpio25, select.EPOLLIN | select.EPOLLET)

    while True:
        # wait here until input
        try:
            events = epoll.poll()
        except KeyboardInterrupt as e:
            if return_after_kbdint:
                return
            else:
                raise e
        except IOError as e:
            # ignore "Interrupted system call" error.
            # I don't really like this solution. Ignoring problems is bad!
            if e.errno != errno.EINTR:
                raise

        # find out where the interrupt came from and put it on the event queue
        if port == pifacecommon.mcp23s17.GPIOA:
            interrupt_flag = chip.intfa.value
        else:
            interrupt_flag = chip.intfb.value

        if interrupt_flag == 0:
            continue  # The interrupt has not been flagged on this board
        else:
            if port == pifacecommon.mcp23s17.GPIOA:
                interrupt_capture = chip.intcapa.value
            else:
                interrupt_capture = chip.intcapb.value
            event_queue.add_event(InterruptEvent(
                interrupt_flag, interrupt_capture, chip, time.time()))

    epoll.close()

def handle_events(
        function_maps, event_queue, event_matches_function_map,
        terminate_signal):
    """Waits for events on the event queue and calls the registered functions.

    :param function_maps: A list of classes that have inheritted from
        :class:`FunctionMap`\ s describing what to do with events.
    :type function_maps: list
    :param event_queue: A queue to put events on.
    :type event_queue: :py:class:`multiprocessing.Queue`
    :param event_matches_function_map: A function that determines if the given
        event and :class:`FunctionMap` match.
    :type event_matches_function_map: function
    :param terminate_signal: The signal that, when placed on the event queue,
        causes this function to exit.
    """
    while True:
        # print("HANDLE: Waiting for events!")
        event = event_queue.get()
        # print("HANDLE: It's an event!")
        if event == terminate_signal:
            return
        # if matching get the callback function, else function is None
        functions = map(
            lambda fm: fm.callback
            if event_matches_function_map(event, fm) else None,
            function_maps)
        # reduce to just the callback functions (remove None)
        # TODO: I think this can just be filter(None, functions)
        functions = filter(lambda f: f is not None, functions)

        for function in functions:
            function(event)

# def clear_interrupts(port):
#     """Clears the interrupt flags by 'read'ing the capture register
#     on all boards.
#     """
#     intcap = INTCAPA if port == GPIOA else INTCAPB
#     for i in range(MAX_BOARDS):
#         read(intcap, i)

# def enable_interrupts(port, pin_map=0xff, hardware_addrs=range(MAX_BOARDS)):
#     """Enables interrupts on the port specified. A pin map can be given to
#     only enable interrupts on some pins. A list of board numbers can also be
#     given to only enable the interrupts on some boards.

#     :param port: The port to enable interrupts on
#         (pifacecommon.core.GPIOA, pifacecommon.core.GPIOB)
#     :type port: int
#     :param pin_map: The pins to enable interrupts on
#     :type pin_map: int
#     :param hardware_addrs: The boards to enable interrupts on.
#     :type hardware_addrs: list
#     """
#     # enable interrupts
#     int_enable_port = GPINTENA if port == GPIOA else GPINTENB
#     for board_index in hardware_addrs:
#         write(pin_map, int_enable_port, board_index)

#     try:
#         bring_gpio_interrupt_into_userspace()
#         set_gpio_interrupt_edge()
#     except Timeout as e:
#         raise InterruptEnableException(
#             "There was an error bringing gpio%d into userspace. %s" %
#             (GPIO_INTERRUPT_PIN, e.message)
#         )

def bring_gpio_interrupt_into_userspace():  # activate gpio interrupt
    """Bring the interrupt pin on the GPIO into Linux userspace."""
    try:
        # is it already there?
        with open(GPIO_INTERRUPT_DEVICE_VALUE):
            return
    except IOError:
        # no, bring it into userspace
        with open(GPIO_EXPORT_FILE, 'w') as export_file:
            export_file.write(str(GPIO_INTERRUPT_PIN))

        wait_until_file_exists(GPIO_INTERRUPT_DEVICE_VALUE)

def deactivate_gpio_interrupt():
    """Remove the GPIO interrupt pin from Linux userspace."""
    with open(GPIO_UNEXPORT_FILE, 'w') as unexport_file:
        unexport_file.write(str(GPIO_INTERRUPT_PIN))

def set_gpio_interrupt_edge(edge='falling'):
    """Set the interrupt edge on the userspace GPIO pin.

    :param edge: The interrupt edge ('none', 'falling', 'rising').
    :type edge: string
    """
    # we're only interested in the falling edge (1 -> 0)
    start_time = time.time()
    time_limit = start_time + FILE_IO_TIMEOUT
    while time.time() < time_limit:
        try:
            with open(GPIO_INTERRUPT_DEVICE_EDGE, 'w') as gpio_edge:
                gpio_edge.write(edge)
                return
        except IOError:
            pass

def wait_until_file_exists(filename):
    """Wait until a file exists.

    :param filename: The name of the file to wait for.
    :type filename: string
    """
    start_time = time.time()
    time_limit = start_time + FILE_IO_TIMEOUT
    while time.time() < time_limit:
        try:
            with open(filename):
                return
        except IOError:
            pass

    raise Timeout("Waiting too long for %s." % filename)

# def disable_interrupts(port):
#     """Disables interrupts for all pins on the port specified.

#     :param port: The port to enable interrupts on
#         (pifacecommon.core.GPIOA, pifacecommon.core.GPIOB)
#     :type port: int
#     """
#     # neither edgez
#     with open(GPIO_INTERRUPT_DEVICE_EDGE, 'w') as gpio25edge:
#         gpio25edge.write('none')

#     # remove the pin from userspace
#     with open(GPIO_UNEXPORT_FILE, 'w') as unexport_file:
#         unexport_file.write(str(GPIO_INTERRUPT_PIN))

#     # disable the interrupt
#     int_enable_port = GPINTENA if port == GPIOA else GPINTENB
#     for board_index in range(MAX_BOARDS):
#         write(0, int_enable_port, board_index)


import posix
import ctypes
from fcntl import ioctl
from .linux_spi_spidev import spi_ioc_transfer, SPI_IOC_MESSAGE

SPIDEV = '/dev/spidev'
SPI_HELP_LINK = "http://piface.github.io/pifacecommon/installation.html" \
    "#enable-the-spi-module"

class SPIInitError(Exception):
    pass

class SPIDevice(object):
    """An SPI Device at /dev/spi<bus>.<chip_select>."""
    def __init__(self, bus=0, chip_select=0, spi_callback=None, speed_hz=100000):
        """Initialises the SPI device file descriptor.

        :param bus: The SPI device bus number
        :type bus: int
        :param chip_select: The SPI device chip_select number
        :param chip_select: int
        :raises: InitError
        """
        self.bus = bus
        self.chip_select = chip_select
        self.spi_callback = spi_callback
        self.speed_hz = speed_hz
        self.fd = None
        spi_device = "%s%d.%d" % (SPIDEV, self.bus, self.chip_select)
        self.open_fd(spi_device)

    # def __del__(self):
    #     if self.fd is not None:
    #         self.close_fd()

    def open_fd(self, spi_device):
        try:
            self.fd = posix.open(spi_device, posix.O_RDWR)
        except OSError as e:
            raise SPIInitError(
                "I can't see %s. Have you enabled the SPI module? (%s)"
                % (spi_device, SPI_HELP_LINK)
            )  # from e  # from is only available in Python 3

    def close_fd(self):
        posix.close(self.fd)
        self.fd = None

    def spisend(self, bytes_to_send):
        """Sends bytes via the SPI bus.

        :param bytes_to_send: The bytes to send on the SPI device.
        :type bytes_to_send: bytes
        :returns: bytes -- returned bytes from SPI device
        :raises: InitError
        """
        # make some buffer space to store reading/writing
        wbuffer = ctypes.create_string_buffer(bytes_to_send,
                                              len(bytes_to_send))
        rbuffer = ctypes.create_string_buffer(len(bytes_to_send))

        # create the spi transfer struct
        transfer = spi_ioc_transfer(
            tx_buf=ctypes.addressof(wbuffer),
            rx_buf=ctypes.addressof(rbuffer),
            len=ctypes.sizeof(wbuffer),
            speed_hz=ctypes.c_uint32(self.speed_hz)
        )

        if self.spi_callback is not None:
            self.spi_callback(bytes_to_send)
        # send the spi command
        ioctl(self.fd, SPI_IOC_MESSAGE(1), transfer)
        return ctypes.string_at(rbuffer, ctypes.sizeof(rbuffer))


"""
Form containers
===============

.. _django-crispy-forms: https://github.com/maraujop/django-crispy-forms
.. _Foundation: http://github.com/zurb/foundation
.. _crispy-forms-foundation-demo: https://github.com/sveetch/crispy-forms-foundation-demo
.. _Abide: http://foundation.zurb.com/docs/components/abide.html

References
    * `Foundation 5 fieldset <http://foundation.zurb.com/sites/docs/v/5.5.3/components/forms.html>`_;
    * `Foundation 5 Accordion <http://foundation.zurb.com/sites/docs/v/5.5.3/components/accordion.html>`_;
    * `Foundation 5 Tabs <http://foundation.zurb.com/sites/docs/v/5.5.3/components/tabs.html>`_;
    * `Foundation 6 fieldset <http://foundation.zurb.com/sites/docs/forms.html#fieldset-styles>`_;
    * `Foundation 6 Accordion <http://foundation.zurb.com/sites/docs/accordion.html>`_;
    * `Foundation 6 Tabs <http://foundation.zurb.com/sites/docs/tabs.html>`_;

"""  # noqa: E501
from random import randint

from django.template.loader import render_to_string

from crispy_forms import layout as crispy_forms_layout
from crispy_forms.utils import render_field, TEMPLATE_PACK
from crispy_forms import bootstrap as crispy_forms_bootstrap
from crispy_forms.compatibility import text_type

__all__ = [
    'Fieldset', 'Container', 'ContainerHolder', 'TabHolder',
    'VerticalTabHolder', 'TabItem', 'AccordionHolder', 'AccordionItem',
]

class Fieldset(crispy_forms_layout.Fieldset):
    """
    It wraps fields in a ``<fieldset>``:

    .. sourcecode:: python

        Fieldset("Text for the legend",
            'form_field_1',
            'form_field_2'
        )

    The first parameter is the text for the fieldset legend. This text is
    context aware, so you can do things like :

    .. sourcecode:: python

        Fieldset("Data for {{ user.username }}",
            'form_field_1',
            'form_field_2'
        )
    """
    template = "%s/layout/fieldset.html"

class Container(crispy_forms_bootstrap.Container):
    """
    Overrides original Container element to get the "active" classname from
    Class attribute ``active_css_class`` so it's compatible with Foundation
    5 and 6.
    """
    css_class = ""
    active_css_class = "active"

    def get_active_css_class(self, template_pack):
        # Foundation-6 addon only which use unusual class name
        if template_pack == 'foundation-6':
            return "is-active"
        return self.active_css_class

    def render(self, form, form_style, context, template_pack=TEMPLATE_PACK,
               **kwargs):
        active_classname = self.get_active_css_class(template_pack)
        if self.active:
            if active_classname and active_classname not in self.css_class:
                self.css_class += ' ' + active_classname
        else:
            self.css_class = self.css_class.replace(active_classname, '')
        return super(Container, self).render(form, form_style, context,
                                             template_pack)

class ContainerHolder(crispy_forms_bootstrap.ContainerHolder):
    pass

class TabHolder(crispy_forms_bootstrap.TabHolder):
    """
    Tabs holder object to wrap Tab item objects in a container:

    .. sourcecode:: python

        TabHolder(
            TabItem('My tab 1', 'form_field_1', 'form_field_2'),
            TabItem('My tab 2', 'form_field_3')
        )

    ``TabHolder`` direct children should allways be a ``TabItem`` layout item.

    A random id is builded for the tab holder if you don't define it using
    ``css_id`` argument.

    The first ``TabItem`` containing a field error will be marked as
    *active* if any, else this will be just the first ``TabItem``.
    """
    template = "%s/layout/tab-holder.html"
    default_active_tab = None

    def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
        """
        Re-implement almost the same code from crispy_forms but passing
        ``form`` instance to item ``render_link`` method.
        """
        links, content = '', ''

        # accordion group needs the parent div id to set `data-parent` (I don't
        # know why). This needs to be a unique id
        if not self.css_id:
            self.css_id = "-".join(["tabsholder",
                                    text_type(randint(1000, 9999))])

        for tab in self.fields:
            tab.active = False

        # Activate item
        self.open_target_group_for_form(form)

        for tab in self.fields:
            content += render_field(
                tab, form, form_style, context, template_pack=template_pack
            )
            links += tab.render_link(form, template_pack)

        context.update({
            'tabs': self,
            'links': links,
            'content': content
        })

        template = self.get_template_name(template_pack)
        return render_to_string(template, context.flatten())

class VerticalTabHolder(TabHolder):
    """
    VerticalTabHolder appends vertical class to TabHolder container
    """
    css_class = 'vertical'

class TabItem(Container):
    """
    Tab item object. It wraps fields in a div whose default class is "tabs" and
    takes a name as first argument.

    Tab item is also responsible of building its associated tab link with its
    ``render_link`` using the ``link_template`` attribute.

    Example:

    .. sourcecode:: python

        TabItem('My tab', 'form_field_1', 'form_field_2', 'form_field_3')

    ``TabItem`` layout item has no real utility out of a ``TabHolder``.
    """
    template = "%s/layout/tab-item.html"
    link_template = "%s/layout/tab-link.html"

    def has_errors(self, form):
        """
        Find tab fields listed as invalid
        """
        return any([fieldname_error for fieldname_error in form.errors.keys()
                    if fieldname_error in self])

    def render_link(self, form, template_pack=TEMPLATE_PACK, **kwargs):
        """
        Render the link for the tab-pane. It must be called after render so
        ``css_class`` is updated with ``active`` class name if needed.
        """
        link_template = self.link_template % template_pack
        return render_to_string(link_template,
                                {
                                    'link': self,
                                    'item_has_errors': self.has_errors(form)
                                })

class AccordionHolder(crispy_forms_bootstrap.Accordion):
    """
    Accordion items holder object to wrap Accordion item objects in a
    container:

    .. sourcecode:: python

        AccordionHolder(
            AccordionItem("group name", "form_field_1", "form_field_2"),
            AccordionItem("another group name", "form_field"),
        )

    ``AccordionHolder`` direct children should allways be a ``AccordionItem``
    layout item.

    A random id is builded for the accordion holder if you don't define it
    using ``css_id`` argument.

    The first ``AccordionItem`` containing a field error will be marked as
    *active* if any, else this will be just the first ``AccordionItem``.
    """
    template = "%s/layout/accordion-holder.html"

    def render(self, form, form_style, context, template_pack=TEMPLATE_PACK,
               **kwargs):
        """
        Re-implement almost the same code from crispy_forms but using
        ``form`` instance to catch field errors.
        """
        content = ''

        # accordion group needs the parent div id to set `data-parent` (I don't
        # know why). This needs to be a unique id
        if not self.css_id:
            self.css_id = "-".join(["accordion",
                                    text_type(randint(1000, 9999))])

        # Active first 'AccordionItem' containing a field error if any, else
        # active first holder item
        self.open_target_group_for_form(form)

        for group in self.fields:
            group.data_parent = self.css_id
            group.item_has_errors = any([fieldname_error for fieldname_error in
                                         form.errors.keys()
                                         if fieldname_error in group])
            content += render_field(
                group, form, form_style, context, template_pack=template_pack,
                **kwargs
            )

        template = self.get_template_name(template_pack)
        context.update({'accordion': self, 'content': content})

        return render_to_string(template, context.flatten())

class AccordionItem(crispy_forms_bootstrap.AccordionGroup):
    """
    Accordion item object. It wraps given fields inside an accordion
    tab. It takes accordion tab name as first argument.

    The item name is also slugified to build an id for the tab if you don't
    define it using ``css_id`` argument.

    Example:

    .. sourcecode:: python

        AccordionItem("group name", "form_field_1", "form_field_2")
    """
    template = "%s/layout/accordion-item.html"


# -*- coding: utf-8 -*-
"""Django application to add 'django-crispy-forms' layout objects for 'Foundation for sites'"""  # noqa: E501
from __future__ import absolute_import, unicode_literals

import os
from setuptools.config import read_configuration

import pkg_resources

PROJECT_DIR = os.path.join(os.path.dirname(__file__), "..")

def _extract_version(package_name):
    """
    Get package version from installed distribution or configuration file if not
    installed
    """
    try:
        return pkg_resources.get_distribution(package_name).version
    except pkg_resources.DistributionNotFound:
        _conf = read_configuration(os.path.join(PROJECT_DIR, "setup.cfg"))
    return _conf["metadata"]["version"]

__version__ = _extract_version("crispy_forms_foundation")


# -*- coding: utf-8 -*-
"""
Views
"""
from django import template
from django.views.generic.edit import FormView
from django.views.generic.base import TemplateView
try:
    # Default 'reverse' path since Django1.10
    from django.urls import reverse
except ImportError:
    # 'reverse' path for Django<1.10
    from django.core.urlresolvers import reverse

from crispy_forms_foundation import __version__ as crispy_foundation_version
from crispy_forms import __version__ as crispy_version
from .forms import FormByFieldsetsForm, FormByTabsForm, FormByAccordionsForm

class CrispyFoundationMixin(object):
    def get_versions(self):
        return {
            "foundation_version": self.kwargs.get('foundation_version'),
            "django_crispy_forms": crispy_version,
            "crispy_forms_foundation": crispy_foundation_version,
        }

    def get_context_data(self, **kwargs):
        context = super(CrispyFoundationMixin, self).get_context_data(**kwargs)
        context.update(self.get_versions())
        return context

class FormContainersMixin(object):
    def get_success_url(self):
        return reverse('crispy-foundation:crispy-demo-success', kwargs={
            'foundation_version': int(self.kwargs.get('foundation_version'))
        })

    def get_form_kwargs(self):
        """
        Pass template pack argument
        """
        kwargs = super(FormContainersMixin, self).get_form_kwargs()
        kwargs.update({
            'pack': "foundation-{}".format(self.kwargs.get('foundation_version'))
        })
        return kwargs

class FormByFieldsetView(FormContainersMixin, CrispyFoundationMixin, FormView):
    template_name = 'crispy_demo/fieldsets.html'
    form_class = FormByFieldsetsForm

class FormByTabView(FormContainersMixin, CrispyFoundationMixin, FormView):
    template_name = 'crispy_demo/tabs.html'
    form_class = FormByTabsForm

class FormByAccordionView(FormContainersMixin, CrispyFoundationMixin, FormView):
    template_name = 'crispy_demo/accordions.html'
    form_class = FormByAccordionsForm

class StaticPage(FormContainersMixin, CrispyFoundationMixin, TemplateView):
    template_name = 'crispy_demo/success.html'