/* JFlexiPix - Java implementation of FlexiPix output library
 *
 * Copyright 2010-2011 Stefan Schuermans <stefan schuermans info>
 *
 * 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 3 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 Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package org.blinkenarea.JFlexiPix;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.regex.*;

/// configuration parser
class Config
{
  /**
   * @brief constructor
   * @param[in] display display to configure
   * @param[in] messageIf message interface to report messages to or null
   */
  Config(Display display, MessageIf messageIf)
  {
    // save constructor parameters
    m_display   = display;
    m_messageIf = messageIf;

    // compile regular expression patterns

    // empty lines: "   # comment"
    m_patternEmpty = Pattern.compile("^" + "\\s*" + "(?:#.*)?" + "$");

    // extract setting and value from "   setting   =   value   # comment"
    m_patternLine = Pattern.compile("^" +
                                    "\\s*" +
                                    "([^\\s=#](?:[^=#]*[^\\s=#])?)" +
                                    "\\s*=\\s*" +
                                    "([^\\s=#](?:[^=#]*[^\\s=#])?)" +
                                    "\\s*" + 
                                    "(?:#.*)?" +
                                    "$");
  }

  /**
   * @brief process distributor from config file
   *
   * @param[in] settingPart2 second half of setting to process
   * @param[in] value value of setting
   */
  void procDistri(String settingPart2, String value)
    throws Exception
  {
    int dist, out, pix;
    Pixel px;

    // get distributor number
    try {
      dist = Integer.parseInt(settingPart2);
    } catch (NumberFormatException e) {
      dist = Constants.distriMaxCnt; // force error in next line
    }
    if (dist >= Constants.distriMaxCnt)
      errorExc("invalid distributor number \"" + settingPart2 + "\"" +
               String.format(" in line %d of config file", m_lineNo));

    // get number of outputs and pixels
    px = PixelParser.parsePixel(value); // abuse for "outputs,pixels"
    if (px == null)
      errorExc("invalid distributor size \"" + value + "\"" +
               String.format(" in line %d of config file", m_lineNo));
    out = px.m_x;
    if (out >= Constants.outputMaxCnt)
      errorExc(String.format("invalid number of outputs \"%d\"" +
                             " in line %d of config file", out, m_lineNo));
    pix = px.m_y;
    if (pix >= Constants.pixelMaxCnt)
      errorExc(String.format("invalid number of pixels \"%d\"" +
                             " in line %d of config file", pix, m_lineNo));

    // check if distributor is already present
    if (m_display.m_distris[dist] != null)
      errorExc(String.format("duplicate definition of distributor \"%d\"" +
                             " in line %d of config file", dist, m_lineNo));

    // create new distributor
    m_display.m_distris[dist] = new Distri(dist, out, pix);

    // count distributors
    m_display.m_distriCnt++;
  }

  /**
   * @brief process distributor address from config file
   *
   * @param[in] settingPart2 second half of setting to process
   * @param[in] value value of setting
   */
  void procDistriAddr(String settingPart2, String value)
    throws Exception
  {
    int dist;
    Distri distri;
    InetSocketAddress addr;

    // get distributor number
    try {
      dist = Integer.parseInt(settingPart2);
    } catch (NumberFormatException e) {
      dist = Constants.distriMaxCnt; // force error in next line
    }
    if (dist >= Constants.distriMaxCnt)
      errorExc(String.format("invalid distributor number \"%d\"" +
                             " in line %d of config file", dist, m_lineNo));

    // get distributor
    distri = m_display.m_distris[dist];
    if (distri == null)
      errorExc(String.format("no distributor with number \"%d\"" +
                             " in line %d of config file", dist, m_lineNo));

    // get address
    addr = AddrParser.parseAddr(value);
    if (addr == null) {
      errorExc("invalid addess \"" + value +
               "\" for distributor with number \"%u\"" +
               String.format(" in line %d of config file", m_lineNo));
    }
    distri.m_addr = addr;
  }

  /**
   * @brief process mapping from config file
   *
   * @param[in] settingPart2 second half of setting to process
   * @param[in] value value of setting
   */
  void procMapping(String settingPart2, String value)
    throws Exception
  {
    Pattern pattern;
    Matcher matcher;
    String strDistri, strChannel, strBase, strFactor, strGamma;
    int dist;
    Distri distri;
    Mapping mapping = null;
    double base = 0.0, factor = 1.0, gamma = 1.0;

    // split setting part 2 into distributor and channel
    pattern = Pattern.compile("^([0-9]+) +([a-z]+)$");
    matcher = pattern.matcher(settingPart2);
    if (!matcher.find())
      errorExc("invalid mapping specifier \"" + settingPart2 + "\"" +
               String.format(" in line %d of config file", m_lineNo));
    strDistri  = matcher.group(1);
    strChannel = matcher.group(2);

    // get distributor number
    try {
      dist = Integer.parseInt(strDistri);
    } catch (NumberFormatException e) {
      dist = Constants.distriMaxCnt; // force error in next line
    }
    if (dist >= Constants.distriMaxCnt)
      errorExc("invalid distributor number \"" + strDistri + "\"" +
               String.format(" in line %d of config file", m_lineNo));

    // get distributor
    distri = m_display.m_distris[dist];
    if (distri == null)
      errorExc(String.format("no distributor with number \"%d\"" +
                             " in line %d of config file", dist, m_lineNo));

    // get channel
    if (strChannel.equals("red"))
      mapping = distri.m_mapRed;
    else if (strChannel.equals("green"))
      mapping = distri.m_mapGreen;
    else if (strChannel.equals("blue"))
      mapping = distri.m_mapBlue;
    else
      errorExc("invalid channel \"" + strChannel + "\"" +
               String.format(" in line %d of config file", m_lineNo));

    // split mapping parameters
    pattern = Pattern.compile("^([-+.eE0-9]+) +([-+.eE0-9]+) +([-+.eE0-9]+)$");
    matcher = pattern.matcher(value);
    if (!matcher.find())
      errorExc("invalid mapping parameters \"" + value + "\"" +
               String.format(" in line %d of config file", m_lineNo));
    strBase   = matcher.group(1);
    strFactor = matcher.group(2);
    strGamma  = matcher.group(3);

    // parse mapping parameters
    try {
      base   = Double.parseDouble(strBase);
      factor = Double.parseDouble(strFactor);
      gamma  = Double.parseDouble(strGamma);
    } catch (NumberFormatException e) {
      errorExc("invalid mapping parameters \"" + value + "\"" +
               String.format(" in line %d of config file", m_lineNo), e);
    }
    if (gamma <= 0.0)
      errorExc(String.format("invalid gamma value \"%f\"" +
                             " in line %d of config file", gamma, m_lineNo));

    // update mapping parameters
    mapping.set(base, factor, gamma);
  }

  /**
   * @brief process pixel from config file
   *
   * @param[in] txtPixel text of pixel to process
   * @param[in] distri distributor
   * @param[in] dist number of distributor
   * @param[in] out number of output
   * @param[in] pix number of pixel
   * @param return if parsing was successful
   */
  boolean procPixel(String txtPixel, Distri distri, int dist, int out, int pix)
  {
    Pixel pixel;
    int idx;

    // get coordinates of pixel
    pixel = PixelParser.parsePixel(txtPixel);
    if (pixel == null) {
      error("invalid pixel \"" + txtPixel + "\"" +
            String.format(" in line %d of config file", m_lineNo));
      return false;
    }

    // check pixel number
    if (pix >= Constants.pixelMaxCnt || pix >= distri.m_pixelCnt) {
      error(String.format("too many pixels (more than %d)" +
                          " in line %d of config file",
                          distri.m_pixelCnt, m_lineNo));
      return false;
    }

    // check that pixel is not yet set
    idx = out * distri.m_pixelCnt + pix;
    if (distri.m_pixels[idx] != null) {
      error(String.format("pixel %d of output %d of distributor %d" +
                          " already set to pixel %d,%d" +
                          " in line %d of config file",
                          pix, out, dist, distri.m_pixels[idx].m_x,
                          distri.m_pixels[idx].m_y, m_lineNo));
      return false;
    }

    // create pixel
    distri.m_pixels[idx] = pixel;

    // count pixels in total
    m_display.m_pixelCnt++;

    return true;
  }

  /**
   * @brief process output from config file
   *
   * @param[in] settingPart2 second half of setting to process
   * @param[in] value value of setting
   */
  void procOutput(String settingPart2, String value)
    throws Exception
  {
    Pixel px;
    int dist, out, pix;
    Distri distri;
    String [] pixels;
    boolean err;

    // get number of distributor and output
    px = PixelParser.parsePixel(settingPart2); // abuse for "dist,out"
    if (px == null)
      errorExc("invalid output specifier \"" + settingPart2 + "\"" +
               String.format(" in line %d of config file", m_lineNo));
    dist = px.m_x;
    if (dist >= Constants.distriMaxCnt)
      errorExc(String.format("invalid distributor number \"%d\"" +
                             " in line %d of config file", dist, m_lineNo));
    out = px.m_y;
    if (out >= Constants.outputMaxCnt)
      errorExc(String.format("invalid output number \"%d\"" +
                             " in line %d of config file", out, m_lineNo));

    // get distributor
    distri = m_display.m_distris[dist];
    if (distri == null)
      errorExc(String.format("no distributor with number \"%d\"" +
                             " in line %d of config file", dist, m_lineNo));

    // check output number
    if (out >= distri.m_outputCnt)
      errorExc(String.format("no output with output number \"%d\"" +
                             " in line %d of config file", out, m_lineNo));

    // count outputs
    m_display.m_outputCnt++;

    // process pixels
    pixels = value.split("\\s+");
    err = false;
    for (pix = 0; pix < pixels.length; ++pix)
      if (!procPixel(pixels[pix], distri, dist, out, pix))
        err = true;
    if (err)
      errorExc(String.format("invalid pixels in line %d of config file",
                             m_lineNo));
  }

  /**
   * @brief process setting from config file
   *
   * @param[in] setting setting to process
   * @param[in] value value of setting
   */
  void procSetting(String setting, String value)
    throws Exception
  {
    InetSocketAddress addr;
    Pixel pix;

    // replace all whitespace in setting with spaces
    setting = setting.replace('\t', ' ').replace('\r', ' ')
                     .replace('\n', ' ').replace('\f', ' ');

    // bind address of UDP output
    if (setting.equals("bindAddr")) {
      addr = AddrParser.parseAddr(value);
      if (addr == null) {
        errorExc("invalid addess \"" + value + "\" for \"" + setting +
                 String.format(" in line %d of config file", m_lineNo));
      }
      info("bind address: " + addr.toString());
      m_display.m_bindAddr = addr;
      return;
    }

    // size of display
    if (setting.equals("size")) {
      pix = PixelParser.parsePixel(value);
      if (pix == null || pix.m_x <= 0 || pix.m_y <= 0) {
        errorExc("invalid value \"" + value + "\" for \"" + setting +
                 String.format(" in line %d of config file", m_lineNo));
      }
      m_display.m_size = pix;
      return;
    }

    // distributor
    if (setting.startsWith("distributor ")) {
      procDistri(setting.substring(12), value);
      return;
    }

    // distributor address
    if (setting.startsWith("distributorAddr ")) {
      procDistriAddr(setting.substring(16), value);
      return;
    }

    // mapping
    if (setting.startsWith("mapping ")) {
      procMapping(setting.substring(8), value);
      return;
    }

    // distributor
    if (setting.startsWith("output ")) {
      procOutput(setting.substring(7), value);
      return;
    }

    // unknown setting
    warning("unknown setting \"" + setting + "\"" +
            String.format(" in line %d of config file", m_lineNo) +
            ", ignored");
  }

  /**
   * @brief process line from config file
   *
   * @param[in] line line to process
   */
  void procLine(String line)
    throws Exception
  {
    Matcher matcher;
    String setting, value;

    // ignore empty lines: "   # comment"
    matcher = m_patternEmpty.matcher(line);
    if (matcher.find())
      return;

    // extract setting and value from "   setting   =   value   # comment"
    matcher = m_patternLine.matcher(line);
    if (!matcher.find()) {
      // line cannot be parsed
      if (m_messageIf != null)
        m_messageIf.message(MsgType.Warn,
                            String.format("invalid line %d in config file," +
                                          " ignored", m_lineNo));
      return;
    }
    setting = matcher.group(1);
    value   = matcher.group(2);

    // process setting
    procSetting(setting, value);
  }

  /**
   * @brief process config file
   *
   * @param[in] configFileName name of config file to read
   */
  void procFile(String configFileName)
    throws Exception
  {
    FileReader fr = null;
    BufferedReader br = null;
    String line;

    // check file name
    if (configFileName.length() == 0)
      errorExc("no config file specified");

    info("using config file \"" + configFileName + "\"");

    // open file
    try {
      fr = new FileReader(configFileName);
      br = new BufferedReader(fr);
    } catch (FileNotFoundException e) {
      errorExc("cannot open config file \"" + configFileName +
               "\" for reading", e);
    }

    // read lines and process them
    m_lineNo = 1;
    while ((line = br.readLine()) != null) {
      procLine(line);
      ++m_lineNo;
    }

    // close file
    br.close();
    fr.close();

    info(String.format("%dx%d input format, ", m_display.m_size.m_x,
                                               m_display.m_size.m_y) +
         String.format("%d distributors, ",    m_display.m_distriCnt ) +
         String.format("%d outputs, ",         m_display.m_outputCnt ) +
         String.format("%d pixels",            m_display.m_pixelCnt));
  }

  /**
   * @brief report error message
   * @param[in] txt error text
   */
  private void error(String txt)
  {
    if (m_messageIf != null)
      m_messageIf.message(MsgType.Err, txt + "\n");
  }

  /**
   * @brief report error message and throw exception
   * @param[in] txt error text
   */
  private void errorExc(String txt)
    throws Exception
  {
    error(txt);
    throw new Exception(txt);
  }

  /**
   * @brief report error message and throw exception
   * @param[in] txt error text
   * @param[in] e reason for error
   */
  private void errorExc(String txt, Exception e)
    throws Exception
  {
    error(txt + ": " + e.getMessage());
    throw new Exception(txt, e);
  }

  /**
   * @brief report warning message
   * @param[in] txt warning text
   */
  private void warning(String txt)
  {
    if (m_messageIf != null)
      m_messageIf.message(MsgType.Warn, txt + "\n");
  }

  /**
   * @brief report information message
   * @param[in] txt information text
   */
  private void info(String txt)
  {
    if (m_messageIf != null)
      m_messageIf.message(MsgType.Info, txt + "\n");
  }

  Display   m_display;      ///< display to configure
  MessageIf m_messageIf;    ///< message interface to report messages to or null
  int       m_lineNo;       ///< current line in config file

  Pattern   m_patternEmpty; ///< empty lines
  Pattern   m_patternLine;  ///< setting lines
}

