/**
 * Copyright (c) 2010-2016, openHAB.org and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.binding.mcp3424.internal;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.openhab.binding.mcp3424.MCP3424BindingProvider;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.binding.BindingProvider;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.pi4j.gpio.extension.mcp.MCP3424GpioProvider;
import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPin;
import com.pi4j.io.gpio.GpioPinAnalogInput;
import com.pi4j.io.gpio.Pin;
import com.pi4j.io.gpio.event.GpioPinAnalogValueChangeEvent;
import com.pi4j.io.gpio.event.GpioPinListenerAnalog;
import com.pi4j.io.i2c.I2CBus;
import com.pi4j.io.i2c.I2CFactory.UnsupportedBusNumberException;
import com.pi4j.wiringpi.GpioUtil;

/**
 * Implement this class if you are going create an actively polling service like
 * querying a Website/Device.
 *
 * @author Alexander Falkenstern
 * @since 1.9.0
 */
public class MCP3424Binding extends AbstractActiveBinding<MCP3424BindingProvider> implements GpioPinListenerAnalog {

    private static final Logger logger = LoggerFactory.getLogger(MCP3424Binding.class);

    private final GpioController gpio;

    private Map<String, GpioPin> gpioPins = new HashMap<String, GpioPin>();

    private static final Map<Integer, MCP3424GpioProvider> mcpProviders = new HashMap<Integer, MCP3424GpioProvider>();

    /**
     * The BundleContext. This is only valid when the bundle is ACTIVE. It is
     * set in the activate() method and must not be accessed anymore once the
     * deactivate() method was called or before activate() was called.
     */
    @SuppressWarnings("unused")
    private BundleContext bundleContext;

    /**
     * the refresh interval which is used to poll values from the mcp3424 server
     * (optional, defaults to 60000ms)
     */
    private long refreshInterval = 60000;

    /**
     * the polling interval mcp3424 check interrupt register (optional, defaults to 50ms)
     */
    private int pollingInterval = 50;

    public MCP3424Binding() {
        // ask for non privileged access (run without root)
        GpioUtil.enableNonPrivilegedAccess();
        // now create a controller
        gpio = GpioFactory.getInstance();
    }

    /**
     * Called by the SCR to activate the component with its configuration read
     * from CAS
     *
     * @param bundleContext
     *            BundleContext of the Bundle that defines this component
     * @param configuration
     *            Configuration properties for this component obtained from the
     *            ConfigAdmin service
     */
    public void activate(final BundleContext bundleContext, final Map<String, Object> configuration) {
        this.bundleContext = bundleContext;

        // to override the default refresh interval one has to add a
        // parameter to openhab.cfg like <bindingName>:refresh=<intervalInMs>
        String refreshIntervalString = (String) configuration.get("refresh");
        if (StringUtils.isNotBlank(refreshIntervalString)) {
            refreshInterval = Long.parseLong(refreshIntervalString);
        }

        // to override the default polling interval one has to add a
        // parameter to openhab.cfg like <bindingName>:polling=<intervalInMs>
        String pollingIntervalString = (String) configuration.get("polling");
        if (StringUtils.isNotBlank(pollingIntervalString)) {
            pollingInterval = Integer.parseInt(pollingIntervalString);
        }

        // read further config parameters here ...
        setProperlyConfigured(true);
        logger.debug("mcp3424 activated and properly configured {}", this.hashCode());
    }

    /**
     * Called by the SCR when the configuration of a binding has been changed
     * through the ConfigAdmin service.
     *
     * @param configuration
     *            Updated configuration properties
     */
    public void modified(final Map<String, Object> configuration) {
        // update the internal configuration accordingly
        logger.debug("mcp3424 modified");
    }

    /**
     * Called by the SCR to deactivate the component when either the
     * configuration is removed or mandatory references are no longer satisfied
     * or the component has simply been stopped.
     *
     * @param reason
     *            Reason code for the deactivation:<br>
     *            <ul>
     *            <li>0 - Unspecified
     *            <li>1 - The component was disabled
     *            <li>2 - A reference became unsatisfied
     *            <li>3 - A configuration was changed
     *            <li>4 - A configuration was deleted
     *            <li>5 - The component was disposed
     *            <li>6 - The bundle was stopped
     *            </ul>
     */
    public void deactivate(final int reason) {
        this.bundleContext = null;
        // deallocate resources here that are no longer needed and
        // should be reset when activating this binding again
        logger.debug("mcp3424 deactivated");
        mcpProviders.clear();
        gpio.shutdown();
    }

    /**
     * @{inheritDoc}
     */
    @Override
    protected long getRefreshInterval() {
        return refreshInterval;
    }

    /**
     * @{inheritDoc}
     */
    @Override
    protected String getName() {
        return "mcp3424 Refresh Service";
    }

    /**
     * @{inheritDoc}
     */
    @Override
    protected void execute() {
    }

    @Override
    public void addBindingProvider(BindingProvider provider) {
        super.addBindingProvider(provider);

        /* first call contains all, better use activate ? if you have providers */
        logger.debug("addBindingProvider: {}", Arrays.toString(provider.getItemNames().toArray()));
        for (String itemName : provider.getItemNames()) {
            bindGpioPin((MCP3424BindingProvider) provider, itemName);
        }
    }

    @Override
    public void removeBindingProvider(BindingProvider provider) {
        super.removeBindingProvider(provider);
        /* shutdown call contains all better use deactivate */
        logger.debug("removeBindingProvider: {}", Arrays.toString(provider.getItemNames().toArray()));
        for (String itemName : provider.getItemNames()) {
            unBindGpioPin((MCP3424BindingProvider) provider, itemName);
        }
    }

    @Override
    public void bindingChanged(BindingProvider provider, String itemName) {
        if (provider instanceof MCP3424BindingProvider) {
            String allItemNames = Arrays.toString(provider.getItemNames().toArray());
            if (provider.getItemNames().contains(itemName)) {
                bindGpioPin((MCP3424BindingProvider) provider, itemName);
                logger.debug("bindingChanged item bound {} - {}", itemName, allItemNames);
            } else {
                unBindGpioPin((MCP3424BindingProvider) provider, itemName);
                logger.debug("bindingChanged item unbound  {} - {}", itemName, allItemNames);
            }
        }
        super.bindingChanged(provider, itemName);
    }

    private void bindGpioPin(MCP3424BindingProvider provider, String itemName) {
        try {
            int address = provider.getBusAddress(itemName);
            MCP3424GpioProvider mcp = mcpProviders.get(address);
            if (mcp == null) {
                try {
                    int gain = provider.getGain(itemName);
                    int resolution = provider.getResolution(itemName);
                    mcp = new MCP3424GpioProvider(I2CBus.BUS_1, address, resolution, gain);
                    mcp.setMonitorInterval(pollingInterval);
                } catch (UnsupportedBusNumberException ex) {
                    throw new IllegalArgumentException("Tried to access not available I2C bus");
                }
                mcpProviders.put(address, mcp);
            }

            mcp.setMonitorEnabled(false);
            Pin pin = provider.getPin(itemName);

            GpioPinAnalogInput input = gpio.provisionAnalogInputPin(mcp, pin, itemName);
            input.addListener(this);
            input.setTag(provider.getItem(itemName));
            gpioPins.put(itemName, input);

            mcp.setEventThreshold(0, input);
            mcp.setMonitorEnabled(true);

            logger.debug("Provisioned analog input for {}", itemName);
        } catch (IOException exception) {
            logger.error("I/O error {}", exception.getMessage());
        }
    }

    private void unBindGpioPin(MCP3424BindingProvider provider, String itemName) {
        GpioPin pin = gpioPins.remove(itemName);
        gpio.unprovisionPin(pin);
        logger.debug("Unbound item {}", itemName);
    }

    /**
     * @{inheritDoc}
     */
    @Override
    protected void internalReceiveCommand(String itemName, Command command) {
        super.internalReceiveCommand(itemName, command);
        // the code being executed when a command was sent on the openHAB
        // event bus goes here. This method is only called if one of the
        // BindingProviders provide a binding for the given 'itemName'.
        logger.debug("internalReceiveCommand({},{}) is called!", itemName, command);
    }

    /**
     * Function will be called by Pi4J, if new conversion result is available.
     * On this time we can notify openhab, that something was changed.
     **/
    @Override
    public void handleGpioPinAnalogValueChangeEvent(GpioPinAnalogValueChangeEvent event) {
        GpioPin pin = event.getPin();
        MCP3424GpioProvider provider = (MCP3424GpioProvider) pin.getProvider();

        if (pin.getTag() instanceof NumberItem) {
            double value = provider.getAnalogValue(pin.getPin());
            try {
                this.eventPublisher.postUpdate(pin.getName(), new DecimalType(value));
            } catch (NumberFormatException exception) {
                logger.warn("Unable to convert '{}' for item {} to number", String.valueOf(value), pin.getName());
            }
            logger.debug("GPIO channel change: {} = {}", pin, value);
        } else if (pin.getTag() instanceof DimmerItem) {
            double value = provider.getPercentValue(pin.getPin());
            try {
                this.eventPublisher.postUpdate(pin.getName(), new PercentType((int) value));
            } catch (NumberFormatException e) {
                logger.warn("Unable to convert '{}' for item {} to number", String.valueOf(value), pin.getName());
            }
            logger.debug("GPIO channel change: {} = {}", pin, value);
        }
    }

    /**
     * @{inheritDoc}
     */
    @Override
    protected void internalReceiveUpdate(String itemName, State newState) {
        super.internalReceiveUpdate(itemName, newState);
        // the code being executed when a state was sent on the openHAB
        // event bus goes here. This method is only called if one of the
        // BindingProviders provide a binding for the given 'itemName'.
        logger.debug("internalReceiveUpdate({},{}) is called", itemName, newState);
    }
}
