/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Copyright (c) The Processing Foundation 2015
  Hardware I/O library developed by Gottfried Haider as part of GSoC 2015

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library 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
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General
  Public License along with this library; if not, write to the
  Free Software Foundation, Inc., 59 Temple Place, Suite 330,
  Boston, MA  02111-1307  USA
*/

package processing.io;

import processing.io.NativeInterface;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;


/**
 * Opens an SPI interface as master<br/>
 * </br>
 * Serial Peripheral Interface (SPI) is a serial bus, commonly used to
 * communicate with sensors and memory devices. It uses four pins: MOSI (Master
 * Out Slave In), MISO (Master In Slave Out), and SCLK (clock signal) - those
 * three are shared among all devices on the bus - as well as one or more SS
 * (Slave Select) pins, that are used for the master to signal to the slave
 * device that it is the desired respondent for the transmission.<br/>
 * <br/>
 * The "master" device initiates a transfer by pulling the SS pin of the "slave"
 * low, and begins outputting a clock signal. In SPI, both the "master" as well
 * as the "slave" device output data at the same time. It is hence not possible
 * to read data without writing some (even if it means outputting zeros or other
 * dummy data).</br>
 * </br>
 * There are multiple possible configuration settings for SPI, see
 * <a href="SPI_settings_.html">settings()</a> for details.</br>
 * <br/>
 * This library supports multiple SPI objects making use of the same SPI
 * interface.
 * 
 * @webref SPI
 * @webBrief Opens an SPI interface as master
 */
public class SPI {

  /**
   *  CPOL=0, CPHA=0, most common
   */
  public static final int MODE0 = 0;
  /**
   *  CPOL=0, CPHA=1
   */
  public static final int MODE1 = 1;
  /**
   *  CPOL=1, CPHA=0
   */
  public static final int MODE2 = 2;
  /**
   *  CPOL=1, CPHA=1
   */
  public static final int MODE3 = 3;
  /**
   *  most significant bit first, most common
   */
  public static final int MSBFIRST = 0;
  /**
   *  least significant bit first
   */
  public static final int LSBFIRST = 1;

  protected int dataOrder = 0;
  protected String dev;
  protected int handle;
  protected int maxSpeed = 500000;
  protected int mode = 0;
  protected static Map<String, String> settings = new HashMap<String, String>();


  /**
   *  Opens an SPI interface as master
   *  @param dev device name
   *  @see list
   *  @webref SPI
   *  @webBrief Opens an SPI interface as master
   */
  public SPI(String dev) {
    NativeInterface.loadLibrary();
    this.dev = dev;

    if (NativeInterface.isSimulated()) {
      return;
    }

    handle = NativeInterface.openDevice("/dev/" + dev);
    if (handle < 0) {
      throw new RuntimeException(NativeInterface.getError(handle));
    }
  }


  /**
   * Closes the SPI interface</br>
   * </br>
   * It is normally not necessary to explicitly close SPI interfaces, as they are
   * closed automatically by the operating system when the sketch exits.</br>
   * </br>
   * Note: It is possible to have two or more objects using the same interface at
   * a time.
   * 
   * @webref SPI
   * @webBrief Closes the SPI interface
   */
  public void close() {
    if (NativeInterface.isSimulated()) {
      return;
    }

    NativeInterface.closeDevice(handle);
    handle = 0;
  }


  protected void finalize() throws Throwable {
    try {
      close();
    } finally {
      super.finalize();
    }
  }


  /**
   *  Lists all available SPI interfaces
   *  @return String array
   *  @webref SPI
   *  @webBrief Lists all available SPI interfaces
   */
  public static String[] list() {
    if (NativeInterface.isSimulated()) {
      // as on the Raspberry Pi
      return new String[]{ "spidev0.0", "spidev0.1" };
    }

    ArrayList<String> devs = new ArrayList<String>();
    File dir = new File("/dev");
    File[] files = dir.listFiles();
    if (files != null) {
      for (File file : files) {
        if (file.getName().startsWith("spidev")) {
          devs.add(file.getName());
        }
      }
    }
    // listFiles() does not guarantee ordering
    String[] tmp = devs.toArray(new String[devs.size()]);
    Arrays.sort(tmp);
    return tmp;
  }


  /**
   * Configures the SPI interface<br/>
   * <br/>
   * The default setting is: 500000, SPI.MSBFIRST, SPI.MODE0
   * 
   * @param maxSpeed  maximum transmission rate in Hz, 500000 (500 kHz) is a
   *                  resonable default
   * @param dataOrder whether data is send with the first- or least-significant
   *                  bit first (SPI.MSBFIRST or SPI.LSBFIRST, the former is more
   *                  common)
   * @param mode      <a href=
   *                  "https://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus#Clock_polarity_and_phase">SPI.MODE0
   *                  to SPI.MODE3</a>
   * @webref SPI
   * @webBrief Configures the SPI interface
   */
  public void settings(int maxSpeed, int dataOrder, int mode) {
    this.maxSpeed = maxSpeed;
    this.dataOrder = dataOrder;
    this.mode = mode;
  }


  /**
   * Transfers data over the SPI bus<br/>
   * <br/>
   * With SPI, data is simultaneously being exchanged between the master device
   * and the slave device. For every byte that is being sent out, there's also one
   * byte being read in.
   * 
   * @param out bytes to send
   * @return bytes read in (array is the same length as out)
   * @webref SPI
   * @webBrief Transfers data over the SPI bus
   */
  public byte[] transfer(byte[] out) {
    if (NativeInterface.isSimulated()) {
      return new byte[out.length];
    }

    // track the current setting per device across multiple instances
    String curSettings = maxSpeed + "-" + dataOrder + "-" + mode;
    if (!curSettings.equals(settings.get(dev))) {
      int ret = NativeInterface.setSpiSettings(handle, maxSpeed, dataOrder, mode);
      if (ret < 0) {
        System.err.println(NativeInterface.getError(handle));
        throw new RuntimeException("Error updating device configuration");
      }
      settings.put(dev, curSettings);
    }

    byte[] in = new byte[out.length];
    int transferred = NativeInterface.transferSpi(handle, out, in);
    if (transferred < 0) {
      throw new RuntimeException(NativeInterface.getError(transferred));
    } else if (transferred < out.length) {
      throw new RuntimeException("Fewer bytes transferred than requested: " + transferred);
    }
    return in;
  }


  /**
   *  Transfers data over the SPI bus
   *  @param out string to send
   *  @return bytes read in (array is the same length as out)
   */
  public byte[] transfer(String out) {
    return transfer(out.getBytes());
  }


  /**
   *  Transfers data over the SPI bus
   *  @param out single byte to send, e.g. numeric literal (0 to 255, or -128 to 127)
   *  @return bytes read in (array is the same length as out)
   */
  public byte[] transfer(int out) {
    if (out < -128 || 255 < out) {
      System.err.println("The transfer function can only operate on a single byte at a time. Call it with a value from 0 to 255, or -128 to 127.");
      throw new RuntimeException("Argument does not fit into a single byte");
    }
    byte[] tmp = new byte[1];
    tmp[0] = (byte)out;
    return transfer(tmp);
  }


  /**
   *  Transfers data over the SPI bus
   *  @param out single byte to send
   *  @return bytes read in (array is the same length as out)
   */
  public byte[] transfer(byte out) {
    // cast to (unsigned) int
    return transfer(out & 0xff);
  }
}
