/* -*- 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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;


/**
 *  @webref
 */
public class PWM {

  int channel;
  String chip;


  /**
   *  Opens a PWM channel
   *  @param channel PWM channel
   *  @see list
   *  @webref
   */
  public PWM(String channel) {
    NativeInterface.loadLibrary();

    int pos = channel.indexOf("/pwm");
    if (pos == -1) {
      throw new IllegalArgumentException("Unsupported channel");
    }
    chip = channel.substring(0, pos);
    this.channel = Integer.parseInt(channel.substring(pos+4));

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

    // export channel through sysfs
    String fn = "/sys/class/pwm/" + chip + "/export";
    int ret = NativeInterface.writeFile(fn, Integer.toString(this.channel));
    if (ret < 0) {
      if (ret == -2) {    // ENOENT
        System.err.println("Make sure your kernel is compiled with PWM_SYSFS enabled and you have the necessary PWM driver for your platform");
      }
      // XXX: check
      if (ret == -22) {   // EINVAL
        System.err.println("PWM channel " + channel + " does not seem to be available on your platform");
      }
      // XXX: check
      if (ret != -16) {   // EBUSY, returned when the pin is already exported
        throw new RuntimeException(fn + ": " + NativeInterface.getError(ret));
      }
    }

    // delay to give udev a chance to change the file permissions behind our back
    // there should really be a cleaner way for this
    try {
      Thread.sleep(500);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }


  /**
   *  Disables the PWM output
   *  @webref
   */
  public void clear() {
    if (NativeInterface.isSimulated()) {
      return;
    }

    String fn = String.format("/sys/class/pwm/%s/pwm%d/enable", chip, channel);
    int ret = NativeInterface.writeFile(fn, "0");
    if (ret < 0) {
      throw new RuntimeException(NativeInterface.getError(ret));
    }
  }


  /**
   *  Gives ownership of a channel back to the operating system
   *  @webref
   */
  public void close() {
    if (NativeInterface.isSimulated()) {
      return;
    }

    // XXX: implicit clear()?
    // XXX: also check GPIO

    String fn = "/sys/class/pwm/" + chip + "/unexport";
    int ret = NativeInterface.writeFile(fn, Integer.toString(channel));
    if (ret < 0) {
      if (ret == -2) {    // ENOENT
        System.err.println("Make sure your kernel is compiled with PWM_SYSFS enabled and you have the necessary PWM driver for your platform");
      }
      // XXX: check
      // EINVAL is also returned when trying to unexport pins that weren't exported to begin with
      throw new RuntimeException(NativeInterface.getError(ret));
    }
  }


  /**
   *  Lists all available PWM channels
   *  @return String array
   *  @webref
   */
  public static String[] list() {
    if (NativeInterface.isSimulated()) {
      return new String[]{ "pwmchip0/pwm0", "pwmchip0/pwm1" };
    }

    ArrayList<String> devs = new ArrayList<String>();
    File dir = new File("/sys/class/pwm");
    File[] chips = dir.listFiles();
    if (chips != null) {
      for (File chip : chips) {
        // get the number of supported channels
        try {
          Path path = Paths.get("/sys/class/pwm/" + chip.getName() + "/npwm");
          String tmp = new String(Files.readAllBytes(path));
          int npwm = Integer.parseInt(tmp.trim());
          for (int i=0; i < npwm; i++) {
            devs.add(chip.getName() + "/pwm" + i);
          }
        } catch (Exception e) {
        }
      }
    }
    // listFiles() does not guarantee ordering
    String[] tmp = devs.toArray(new String[devs.size()]);
    Arrays.sort(tmp);
    return tmp;
  }


  /**
   *  Enables the PWM output
   *  @param period cycle period in Hz
   *  @param duty duty cycle, 0.0 (always off) to 1.0 (always on)
   *  @webref
   */
  public void set(int period, float duty) {
    if (NativeInterface.isSimulated()) {
      return;
    }

    // set period
    String fn = fn = String.format("/sys/class/pwm/%s/pwm%d/period", chip, channel);
    // convert to nanoseconds
    int ret = NativeInterface.writeFile(fn, String.format("%d", (int)(1000000000 / period)));
    if (ret < 0) {
      throw new RuntimeException(fn + ": " + NativeInterface.getError(ret));
    }

    // set duty cycle
    fn = fn = String.format("/sys/class/pwm/%s/pwm%d/duty_cycle", chip, channel);
    if (duty < 0.0 || 1.0 < duty) {
      System.err.println("Duty cycle must be between 0.0 and 1.0.");
      throw new IllegalArgumentException("Illegal argument");
    }
    // convert to nanoseconds
    ret = NativeInterface.writeFile(fn, String.format("%d", (int)((1000000000 * duty) / period)));
    if (ret < 0) {
      throw new RuntimeException(fn + ": " + NativeInterface.getError(ret));
    }

    // enable output
    fn = String.format("/sys/class/pwm/%s/pwm%d/enable", chip, channel);
    ret = NativeInterface.writeFile(fn, "1");
    if (ret < 0) {
      throw new RuntimeException(fn + ": " + NativeInterface.getError(ret));
    }
  }


  /**
   *  Enables the PWM output with a preset period of 1 kHz
   *  @param duty duty cycle, 0.0 (always off) to 1.0 (always on)
   *  @webref
   */
  public void set(float duty) {
    set(1000, duty);
  }
}
