/***
 * Copyright 2002-2010 jamod development team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ***/

package net.wimpi.modbus.facade;

import net.wimpi.modbus.ModbusException;
import net.wimpi.modbus.io.ModbusTCPTransaction;
import net.wimpi.modbus.msg.*;
import net.wimpi.modbus.net.TCPMasterConnection;
import net.wimpi.modbus.procimg.InputRegister;
import net.wimpi.modbus.procimg.Register;
import net.wimpi.modbus.util.BitVector;

import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * Modbus/TCP Master facade.
 *
 * @author Dieter Wimberger
 * @version @version@ (@date@)
 */
public class ModbusTCPMaster {

  private TCPMasterConnection m_Connection;
  private InetAddress m_SlaveAddress;
  private ModbusTCPTransaction m_Transaction;
  private ReadCoilsRequest m_ReadCoilsRequest;
  private ReadInputDiscretesRequest m_ReadInputDiscretesRequest;
  private WriteCoilRequest m_WriteCoilRequest;
  private WriteMultipleCoilsRequest m_WriteMultipleCoilsRequest;
  private ReadInputRegistersRequest m_ReadInputRegistersRequest;
  private ReadMultipleRegistersRequest m_ReadMultipleRegistersRequest;
  private WriteSingleRegisterRequest m_WriteSingleRegisterRequest;
  private WriteMultipleRegistersRequest m_WriteMultipleRegistersRequest;
  private boolean m_Reconnecting = false;

  /**
   * Constructs a new master facade instance for communication
   * with a given slave.
   *
   * @param addr an internet address as resolvable IP name or IP number,
   *             specifying the slave to communicate with.
   */
  public ModbusTCPMaster(String addr) {
    try {
      m_SlaveAddress = InetAddress.getByName(addr);
      m_Connection = new TCPMasterConnection(m_SlaveAddress);
      m_ReadCoilsRequest = new ReadCoilsRequest();
      m_ReadInputDiscretesRequest = new ReadInputDiscretesRequest();
      m_WriteCoilRequest = new WriteCoilRequest();
      m_WriteMultipleCoilsRequest = new WriteMultipleCoilsRequest();
      m_ReadInputRegistersRequest = new ReadInputRegistersRequest();
      m_ReadMultipleRegistersRequest = new ReadMultipleRegistersRequest();
      m_WriteSingleRegisterRequest = new WriteSingleRegisterRequest();
      m_WriteMultipleRegistersRequest = new WriteMultipleRegistersRequest();

    } catch (UnknownHostException e) {
      throw new RuntimeException(e.getMessage());
    }
  }//constructor

  /**
   * Constructs a new master facade instance for communication
   * with a given slave.
   *
   * @param addr an internet address as resolvable IP name or IP number,
   *             specifying the slave to communicate with.
   * @param port the port the slave is listening to.
   */
  public ModbusTCPMaster(String addr, int port) {
    this(addr);
    m_Connection.setPort(port);
  }//constructor

  /**
   * Connects this <tt>ModbusTCPMaster</tt> with the slave.
   *
   * @throws Exception if the connection cannot be established.
   */
  public void connect()
      throws Exception {
    if (m_Connection != null && !m_Connection.isConnected()) {
      m_Connection.connect();
      m_Transaction = new ModbusTCPTransaction(m_Connection);
      m_Transaction.setReconnecting(m_Reconnecting);
    }
  }//connect

  /**
   * Disconnects this <tt>ModbusTCPMaster</tt> from the slave.
   */
  public void disconnect() {
    if (m_Connection != null && m_Connection.isConnected()) {
      m_Connection.close();
      m_Transaction = null;
    }
  }//disconnect

  /**
   * Sets the flag that specifies whether to maintain a
   * constant connection or reconnect for every transaction.
   *
   * @param b true if a new connection should be established for each
   *          transaction, false otherwise.
   */
  public void setReconnecting(boolean b) {
    m_Reconnecting = b;
    if (m_Transaction != null) {
      m_Transaction.setReconnecting(b);
    }
  }//setReconnecting

  /**
   * Tests if a constant connection is maintained or if a new
   * connection is established for every transaction.
   *
   * @return true if a new connection should be established for each
   *         transaction, false otherwise.
   */
  public boolean isReconnecting() {
    return m_Reconnecting;
  }//isReconnecting

  /**
   * Reads a given number of coil states from the slave.
   * <p/>
   * Note that the number of bits in the bit vector will be
   * forced to the number originally requested.
   *
   * @param ref   the offset of the coil to start reading from.
   * @param count the number of coil states to be read.
   * @return a <tt>BitVector</tt> instance holding the
   *         received coil states.
   * @throws ModbusException if an I/O error, a slave exception or
   *                         a transaction error occurs.
   */
  public synchronized BitVector readCoils(int ref, int count)
      throws ModbusException {
    m_ReadCoilsRequest.setReference(ref);
    m_ReadCoilsRequest.setBitCount(count);
    m_Transaction.setRequest(m_ReadCoilsRequest);
    m_Transaction.execute();
    BitVector bv = ((ReadCoilsResponse) m_Transaction.getResponse()).getCoils();
    bv.forceSize(count);
    return bv;
  }//readCoils

  /**
   * Writes a coil state to the slave.
   *
   * @param unitid the slave unit id.
   * @param ref    the offset of the coil to be written.
   * @param state  the coil state to be written.
   * @return the state of the coil as returned from the slave.
   * @throws ModbusException if an I/O error, a slave exception or
   *                         a transaction error occurs.
   */
  public synchronized boolean writeCoil(int unitid, int ref, boolean state)
      throws ModbusException {
    m_WriteCoilRequest.setUnitID(unitid);
    m_WriteCoilRequest.setReference(ref);
    m_WriteCoilRequest.setCoil(state);
    m_Transaction.setRequest(m_WriteCoilRequest);
    m_Transaction.execute();
    return ((WriteCoilResponse) m_Transaction.getResponse()).getCoil();
  }//writeCoil

  /**
   * Writes a given number of coil states to the slave.
   * <p/>
   * Note that the number of coils to be written is given
   * implicitly, through {@link BitVector#size()}.
   *
   * @param ref   the offset of the coil to start writing to.
   * @param coils a <tt>BitVector</tt> which holds the coil states to be written.
   * @throws ModbusException if an I/O error, a slave exception or
   *                         a transaction error occurs.
   */
  public synchronized void writeMultipleCoils(int ref, BitVector coils)
      throws ModbusException {
    m_WriteMultipleCoilsRequest.setReference(ref);
    m_WriteMultipleCoilsRequest.setCoils(coils);
    m_Transaction.setRequest(m_WriteMultipleCoilsRequest);
    m_Transaction.execute();
  }//writeMultipleCoils

  /**
   * Reads a given number of input discrete states from the slave.
   * <p/>
   * Note that the number of bits in the bit vector will be
   * forced to the number originally requested.
   *
   * @param ref   the offset of the input discrete to start reading from.
   * @param count the number of input discrete states to be read.
   * @return a <tt>BitVector</tt> instance holding the received input discrete
   *         states.
   * @throws ModbusException if an I/O error, a slave exception or
   *                         a transaction error occurs.
   */
  public synchronized BitVector readInputDiscretes(int ref, int count)
      throws ModbusException {
    m_ReadInputDiscretesRequest.setReference(ref);
    m_ReadInputDiscretesRequest.setBitCount(count);
    m_Transaction.setRequest(m_ReadInputDiscretesRequest);
    m_Transaction.execute();
    BitVector bv = ((ReadInputDiscretesResponse) m_Transaction.getResponse()).getDiscretes();
    bv.forceSize(count);
    return bv;
  }//readInputDiscretes


  /**
   * Reads a given number of input registers from the slave.
   * <p/>
   * Note that the number of input registers returned (i.e. array length)
   * will be according to the number received in the slave response.
   *
   * @param ref   the offset of the input register to start reading from.
   * @param count the number of input registers to be read.
   * @return a <tt>InputRegister[]</tt> with the received input registers.
   * @throws ModbusException if an I/O error, a slave exception or
   *                         a transaction error occurs.
   */
  public synchronized InputRegister[] readInputRegisters(int ref, int count)
      throws ModbusException {
    m_ReadInputRegistersRequest.setReference(ref);
    m_ReadInputRegistersRequest.setWordCount(count);
    m_Transaction.setRequest(m_ReadInputRegistersRequest);
    m_Transaction.execute();
    return ((ReadInputRegistersResponse) m_Transaction.getResponse()).getRegisters();
  }//readInputRegisters

  /**
   * Reads a given number of registers from the slave.
   * <p/>
   * Note that the number of registers returned (i.e. array length)
   * will be according to the number received in the slave response.
   *
   * @param ref   the offset of the register to start reading from.
   * @param count the number of registers to be read.
   * @return a <tt>Register[]</tt> holding the received registers.
   * @throws ModbusException if an I/O error, a slave exception or
   *                         a transaction error occurs.
   */
  public synchronized Register[] readMultipleRegisters(int ref, int count)
      throws ModbusException {
    m_ReadMultipleRegistersRequest.setReference(ref);
    m_ReadMultipleRegistersRequest.setWordCount(count);
    m_Transaction.setRequest(m_ReadMultipleRegistersRequest);
    m_Transaction.execute();
    return ((ReadMultipleRegistersResponse) m_Transaction.getResponse()).getRegisters();
  }//readMultipleRegisters

  /**
   * Writes a single register to the slave.
   *
   * @param ref      the offset of the register to be written.
   * @param register a <tt>Register</tt> holding the value of the register
   *                 to be written.
   * @throws ModbusException if an I/O error, a slave exception or
   *                         a transaction error occurs.
   */
  public synchronized void writeSingleRegister(int ref, Register register)
      throws ModbusException {
    m_WriteSingleRegisterRequest.setReference(ref);
    m_WriteSingleRegisterRequest.setRegister(register);
    m_Transaction.setRequest(m_WriteSingleRegisterRequest);
    m_Transaction.execute();
  }//writeSingleRegister

  /**
   * Writes a number of registers to the slave.
   *
   * @param ref       the offset of the register to start writing to.
   * @param registers a <tt>Register[]</tt> holding the values of
   *                  the registers to be written.
   * @throws ModbusException if an I/O error, a slave exception or
   *                         a transaction error occurs.
   */
  public synchronized void writeMultipleRegisters(int ref, Register[] registers)
      throws ModbusException {
    m_WriteMultipleRegistersRequest.setReference(ref);
    m_WriteMultipleRegistersRequest.setRegisters(registers);
    m_Transaction.setRequest(m_WriteMultipleRegistersRequest);
    m_Transaction.execute();
  }//writeMultipleRegisters

}//class ModbusTCPMaster
