/**
 * This file is part of VisiCut.
 * Copyright (C) 2011 Thomas Oster <thomas.oster@rwth-aachen.de>
 * RWTH Aachen University - 52062 Aachen, Germany
 * 
 *     VisiCut 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 3 of the License, or
 *     (at your option) any later version.
 * 
 *    VisiCut 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 VisiCut.  If not, see <http://www.gnu.org/licenses/>.
 **/
/**
 * Known Limitations:
 * - If there is Raster and Raster3d Part in one job, the speed from 3d raster
 * is taken for both and eventually other side effects:
 * IT IS NOT RECOMMENDED TO USE 3D-Raster and Raster in the same Job
 */
package com.t_oster.liblasercut.drivers;

import com.t_oster.liblasercut.*;
import com.t_oster.liblasercut.platform.Point;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 *
 * @author Thomas Oster <thomas.oster@rwth-aachen.de>
 */
abstract class EpilogCutter extends LaserCutter
{

  public static boolean SIMULATE_COMMUNICATION = false;
  public static final int NETWORK_TIMEOUT = 3000;
  /* Resolutions in DPI */
  
  private static final int MINFOCUS = -500;//Minimal focus value (not mm)
  private static final int MAXFOCUS = 500;//Maximal focus value (not mm)
  private static final double FOCUSWIDTH = 0.0252;//How much mm/unit the focus values are
  private String hostname = "10.0.0.1";
  private int port = 515;
  private Socket connection;
  private boolean autofocus = false;
  private InputStream in;
  private OutputStream out;

  private int mm2focus(float mm)
  {
    return (int) (mm / FOCUSWIDTH);
  }

  private float focus2mm(int focus)
  {
    return (float) (focus * FOCUSWIDTH);
  }

  public EpilogCutter()
  {
  }

  public EpilogCutter(String hostname)
  {
    this.hostname = hostname;
  }

  public String getHostname()
  {
    return this.hostname;
  }

  public void setHostname(String hostname)
  {
    this.hostname = hostname;
  }

  @Override
  public boolean isAutoFocus()
  {
    return this.autofocus;
  }
  
  public void setAutoFocus(boolean af)
  {
    this.autofocus = af;
  }
  
  private void waitForResponse(int expected) throws IOException, Exception
  {
    waitForResponse(expected, 3);
  }

  private void waitForResponse(int expected, int timeout) throws IOException, Exception
  {
    if (SIMULATE_COMMUNICATION)
    {
      return;
    }
    int result;
    out.flush();
    for (int i = 0; i < timeout * 10; i++)
    {
      if (in.available() > 0)
      {
        result = in.read();
        if (result == -1)
        {
          throw new IOException("End of Stream");
        }
        if (result != expected)
        {
          throw new Exception("unexpected Response: " + result);
        }
        return;
      }
      else
      {
        Thread.sleep(100 * timeout);
      }
    }
    throw new Exception("Timeout");

  }

  private byte[] generatePjlHeader(LaserJob job) throws UnsupportedEncodingException
  {
    ByteArrayOutputStream result = new ByteArrayOutputStream();
    PrintStream out = new PrintStream(result, true, "US-ASCII");
    /* Print the printer job language header. */
    out.printf("\033%%-12345X@PJL JOB NAME=%s\r\n", job.getTitle());
    out.printf("\033E@PJL ENTER LANGUAGE=PCL\r\n");
    if (this.isAutoFocus())
    {
      /* Set autofocus on. */
      out.printf("\033&y1A");  
    }
    else
    {
      /* Set autofocus off. */
      out.printf("\033&y0A");  
    }
    /* Set focus to 0. */
    out.printf("\033&y0C");
    /* UNKNOWN */
    out.printf("\033&y0Z");
    /* Left (long-edge) offset registration.  Adjusts the position of the
     * logical page across the width of the page.
     */
    out.printf("\033&l0U");
    /* Top (short-edge) offset registration.  Adjusts the position of the
     * logical page across the length of the page.
     */
    out.printf("\033&l0Z");
    /* Resolution of the print. Number of Units/Inch*/
    out.printf("\033&u%dD", job.getResolution());
    /* X position = 0 */
    out.printf("\033*p0X");
    /* Y position = 0 */
    out.printf("\033*p0Y");
    /* PCL/RasterGraphics resolution. */
    out.printf("\033*t%dR", job.getResolution());
    return result.toByteArray();
  }

  private byte[] generatePjlFooter() throws UnsupportedEncodingException
  {
    ByteArrayOutputStream result = new ByteArrayOutputStream();
    PrintStream out = new PrintStream(result, true, "US-ASCII");

    /* Footer for printer job language. */
    /* Reset */
    out.printf("\033E");
    /* Exit language. */
    out.printf("\033%%-12345X");
    /* End job. */
    out.printf("@PJL EOJ \r\n");
    return result.toByteArray();
  }

  private void sendPjlJob(LaserJob job, byte[] pjlData) throws UnknownHostException, UnsupportedEncodingException, IOException, Exception
  {
    String localhost;
    try
    {
      localhost = java.net.InetAddress.getLocalHost().getHostName();
    }
    catch (UnknownHostException e)
    {
      localhost = "unknown";
    }
    PrintStream out = new PrintStream(this.out, true, "US-ASCII");
    out.print("\002\n");
    waitForResponse(0);
    ByteArrayOutputStream tmp = new ByteArrayOutputStream();
    PrintStream stmp = new PrintStream(tmp, true, "US-ASCII");
    stmp.printf("H%s\n", localhost);
    stmp.printf("P%s\n", job.getUser());
    stmp.printf("J%s\n", job.getTitle());
    stmp.printf("ldfA%s%s\n", job.getName(), localhost);
    stmp.printf("UdfA%s%s\n", job.getName(), localhost);
    stmp.printf("N%s\n", job.getTitle());
    out.printf("\002%d cfA%s%s\n", tmp.toByteArray().length, job.getName(), localhost);
    waitForResponse(0);
    out.write(tmp.toByteArray());
    out.append((char) 0);
    waitForResponse(0);
    /* Send the Job length and name to the queue */
    out.printf("\003%d dfA%s%s\n", pjlData.length, job.getName(), localhost);
    waitForResponse(0);
    /* Send the real PJL Job */
    out.write(pjlData);
    waitForResponse(0);
  }

  private void connect() throws IOException, SocketTimeoutException
  {
    if (SIMULATE_COMMUNICATION)
    {
      out = System.out;
    }
    else
    {
      connection = new Socket();
      connection.connect(new InetSocketAddress(hostname, port), NETWORK_TIMEOUT);
      in = new BufferedInputStream(connection.getInputStream());
      out = new BufferedOutputStream(connection.getOutputStream());
    }
  }

  private void disconnect() throws IOException
  {
    if (!SIMULATE_COMMUNICATION)
    {
      in.close();
      out.close();
    }
  }

  @Override
  protected void checkJob(LaserJob job) throws IllegalJobException
  {
    super.checkJob(job);
    for (JobPart p : job.getParts())
    {
      if (p instanceof VectorPart)
      {
        for (VectorCommand cmd : ((VectorPart) p).getCommandList())
        {
          if (cmd.getType() == VectorCommand.CmdType.SETPROPERTY)
          {
            if (!(cmd.getProperty() instanceof PowerSpeedFocusFrequencyProperty))
            {
              throw new IllegalJobException("This driver expects Power,Speed,Frequency and Focus as settings");
            }
            float focus = ((PowerSpeedFocusFrequencyProperty) cmd.getProperty()).getFocus();
            if (mm2focus(focus) > MAXFOCUS || (mm2focus(focus)) < MINFOCUS)
            {
              throw new IllegalJobException("Illegal Focus value. This Lasercutter supports values between"
                + focus2mm(MINFOCUS) + "mm to " + focus2mm(MAXFOCUS) + "mm.");
            }
          }
        }
      }
      if (p instanceof RasterPart)
      {
        RasterPart rp = ((RasterPart) p);
        for (int i = 0; i < rp.getRasterCount(); i++)
        {
          if (rp.getLaserProperty(i) != null && !(rp.getLaserProperty(i) instanceof PowerSpeedFocusProperty))
          {
            throw new IllegalJobException("This driver expects Power,Speed and Focus as settings");
          }
          float focus = rp.getLaserProperty(i) == null ? 0 : ((PowerSpeedFocusProperty) rp.getLaserProperty(i)).getFocus();
          if (mm2focus(focus) > MAXFOCUS || (mm2focus(focus)) < MINFOCUS)
          {
            throw new IllegalJobException("Illegal Focus value. This Lasercutter supports values between"
              + focus2mm(MINFOCUS) + "mm to " + focus2mm(MAXFOCUS) + "mm.");
          }
        }
      }
      if (p instanceof Raster3dPart)
      {
        Raster3dPart rp = (Raster3dPart) p;
        for (int i = 0; i < rp.getRasterCount(); i++)
        {
          if (rp.getLaserProperty(i) != null && !(rp.getLaserProperty(i) instanceof PowerSpeedFocusProperty))
          {
            throw new IllegalJobException("This driver expects Power,Speed and Focus as settings");
          }
          float focus = rp.getLaserProperty(i) == null ? 0 : ((PowerSpeedFocusProperty) rp.getLaserProperty(i)).getFocus();
          if (mm2focus(focus) > MAXFOCUS || (mm2focus(focus)) < MINFOCUS)
          {
            throw new IllegalJobException("Illegal Focus value. This Lasercutter supports values between"
              + focus2mm(MINFOCUS) + "mm to " + focus2mm(MAXFOCUS) + "mm.");
          }
        }
      }
    }
  }

  @Override
  public void sendJob(LaserJob job, ProgressListener pl) throws IllegalJobException, SocketTimeoutException, UnsupportedEncodingException, IOException, UnknownHostException, Exception
  {
    pl.progressChanged(this, 0);
    pl.taskChanged(this, "checking job");
    //Perform santiy checks
    checkJob(job);
    pl.taskChanged(this, "generating data");
    //Generate all the data
    byte[] pjlData = generatePjlData(job);
    pl.progressChanged(this, 40);
    //connect to lasercutter
    pl.taskChanged(this, "connecting");
    connect();
    pl.progressChanged(this, 60);
    //send job
    pl.taskChanged(this, "sending");
    sendPjlJob(job, pjlData);
    pl.progressChanged(this, 90);
    //disconnect
    disconnect();
    pl.progressChanged(this, 100);
  }


  @Override
  abstract public List<Integer> getResolutions();
  

  /**
   * Encodes the given line of the given image in TIFF Packbyte encoding
   */
  public List<Byte> encode(List<Byte> line)
  {
    int idx = 0;
    int r = line.size();
    List<Byte> result = new LinkedList<Byte>();
    while (idx < r)
    {
      int p;
      p = idx + 1;
      while (p < r && p < idx + 128 && line.get(p) == line.get(idx))
      {
        p++;
      }
      if (p - idx >= 2)
      {
        // run length
        result.add((byte) (1 - (p - idx)));
        result.add((byte) line.get(idx));
        idx = p;
      }
      else
      {
        p = idx;
        while (p < r && p < idx + 127
          && (p + 1 == r || line.get(p)
          != line.get(p + 1)))
        {
          p++;
        }
        result.add((byte) (p - idx - 1));
        while (idx < p)
        {
          result.add((byte) (line.get(idx++)));
        }
      }
    }
    return result;
  }

  private byte[] generateRaster3dPCL(LaserJob job, Raster3dPart rp) throws UnsupportedEncodingException, IOException
  {
    ByteArrayOutputStream result = new ByteArrayOutputStream();
    PrintStream out = new PrintStream(result, true, "US-ASCII");
    PowerSpeedFocusProperty curprop = null;
    if (rp != null && rp.getRasterCount() > 0)
    {
      if (rp.getRasterCount() > 0)
      {
        curprop = (PowerSpeedFocusProperty) rp.getLaserProperty(0);
      }
      /* Raster Orientation: Printed in current direction */
      out.printf("\033*r0F");
      /* Raster power */
      out.printf("\033&y%dP", curprop.getPower());
      /* Raster speed */
      out.printf("\033&z%dS", curprop.getSpeed());
      /* Focus */
      out.printf("\033&y%dA", mm2focus(curprop.getFocus()));

      out.printf("\033*r%dT", rp != null ? rp.getMaxY() : 10);//height);
      out.printf("\033*r%dS", rp != null ? rp.getMaxX() : 10);//width);
            /* Raster compression:
       *  2 = TIFF encoding
       *  7 = TIFF encoding, 3d-mode,
       *
       * Wahrscheinlich:
       * 2M = Bitweise, also 1=dot 0=nodot (standard raster)
       * 7MLT = Byteweise 0= no power 100=full power (3d raster)
       */
      out.printf("\033*b%dMLT", 7);
      /* Raster direction (1 = up, 0=down) */
      out.printf("\033&y%dO", 0);
      /* start at current position */
      out.printf("\033*r1A");

      for (int i = 0; rp != null && i < rp.getRasterCount(); i++)
      {
        PowerSpeedFocusProperty newprop = (PowerSpeedFocusProperty) rp.getLaserProperty(i);
        if (newprop.getPower() != curprop.getPower())
        {
          /* Raster power */
          out.printf("\033&y%dP", newprop.getPower());
        }
        if (newprop.getSpeed() != curprop.getSpeed())
        {
          /* Raster speed */
          out.printf("\033&z%dS", newprop.getSpeed());
        }
        if (newprop.getFocus() != curprop.getFocus())
        {
          /* Focus  */
          out.printf("\033&y%dA", mm2focus(newprop.getFocus()));
        }
        curprop = newprop;
        Point sp = rp.getRasterStart(i);
        boolean leftToRight = true;
        for (int y = 0; y < rp.getRasterHeight(i); y++)
        {

          List<Byte> line = rp.getInvertedRasterLine(i, y);
          for (int n = 0; n < line.size(); n++)
          {//Apperantly the other power settings are ignored, so we have to scale
            int x = line.get(n);
            x = x >= 0 ? x : 256 + x;
            int scalex = x * curprop.getPower() / 100;
            byte bx = (byte) (scalex < 128 ? scalex : scalex - 256);
            line.set(n, bx);
          }
          //Remove leading zeroes, but keep track of the offset
          int jump = 0;

          while (line.size() > 0 && line.get(0) == 0)
          {
            line.remove(0);
            jump++;
          }
          if (line.size() > 0)
          {
            out.printf("\033*p%dX", sp.x + jump);
            out.printf("\033*p%dY", sp.y + y);
            if (leftToRight)
            {
              out.printf("\033*b%dA", line.size());
            }
            else
            {
              out.printf("\033*b%dA", -line.size());
              Collections.reverse(line);
            }
            line = encode(line);
            int len = line.size();
            int pcks = len / 8;
            if (len % 8 > 0)
            {
              pcks++;
            }
            out.printf("\033*b%dW", pcks * 8);
            for (byte s : line)
            {
              out.write(s);
            }
            for (int k = 0; k < 8 - (len % 8); k++)
            {
              out.write((byte) 128);
            }
            leftToRight = !leftToRight;
          }
        }

      }
      out.printf("\033*rC");       // end raster
    }
    return result.toByteArray();
  }

  private byte[] generateRasterPCL(LaserJob job, RasterPart rp) throws UnsupportedEncodingException, IOException
  {

    PowerSpeedFocusProperty curprop = null;
    if (rp != null && rp.getRasterCount() > 0)
    {
      curprop = (PowerSpeedFocusProperty) rp.getLaserProperty(0);
    }
    else
    {
      curprop = new PowerSpeedFocusProperty();
    }
    ByteArrayOutputStream result = new ByteArrayOutputStream();
    PrintStream out = new PrintStream(result, true, "US-ASCII");
    /* Raster Orientation: Printed in current direction */
    out.printf("\033*r0F");
    /* Raster power */
    out.printf("\033&y%dP", curprop.getPower());
    /* Raster speed */
    out.printf("\033&z%dS", curprop.getSpeed());
    /* Focus */
    out.printf("\033&y%dA", mm2focus(curprop.getFocus()));

    out.printf("\033*r%dT", rp != null ? rp.getMaxY() : 10);//height);
    out.printf("\033*r%dS", rp != null ? rp.getMaxX() : 10);//width);
        /* Raster compression:
     *  2 = TIFF encoding
     *  7 = TIFF encoding, 3d-mode,
     *
     * Wahrscheinlich:
     * 2M = Bitweise, also 1=dot 0=nodot (standard raster)
     * 7MLT = Byteweise 0= no power 100=full power (3d raster)
     */
    out.printf("\033*b2M");
    /* Raster direction (1 = up, 0=down) */
    out.printf("\033&y%dO", 0);
    /* start at current position */
    out.printf("\033*r1A");

    for (int i = 0; rp != null && i < rp.getRasterCount(); i++)
    {
      //TODO: Test if new Settings are applied
      PowerSpeedFocusProperty newprop = (PowerSpeedFocusProperty) rp.getLaserProperty(i);
      if (newprop.getPower() != curprop.getPower())
      {
        /* Raster power */
        out.printf("\033&y%dP", newprop.getPower());
      }
      if (newprop.getSpeed() != curprop.getSpeed())
      {
        /* Raster speed */
        out.printf("\033&z%dS", newprop.getSpeed());
      }
      if (newprop.getFocus() != curprop.getFocus())
      {
        /* Focus  */
        out.printf("\033&y%dA", mm2focus(newprop.getFocus()));
      }
      curprop = newprop;
      Point sp = rp.getRasterStart(i);
      boolean leftToRight = true;
      for (int y = 0; y < rp.getRasterHeight(i); y++)
      {

        List<Byte> line = rp.getRasterLine(i, y);
        //Remove leading zeroes, but keep track of the offset
        int jump = 0;

        while (line.size() > 0 && line.get(0) == 0)
        {
          line.remove(0);
          jump++;
        }
        //Remove trailing zeroes
        while (line.size() > 0 && line.get(line.size()-1) == 0)
        {
          line.remove(line.size()-1);
        }
        if (line.size() > 0)
        {
          out.printf("\033*p%dX", sp.x + jump * 8);
          out.printf("\033*p%dY", sp.y + y);
          if (leftToRight)
          {
            out.printf("\033*b%dA", line.size());
          }
          else
          {
            out.printf("\033*b%dA", -line.size());
            Collections.reverse(line);
          }
          line = encode(line);
          int len = line.size();
          int pcks = len / 8;
          if (len % 8 > 0)
          {
            pcks++;
          }
          /**
           * Number of Pixels in a row??
           * or b2m%dW for TIFF encoding?
           * Or number of Bytes in a row? who knows
           * in ctrl-cut its number of packed bytes
           */
          out.printf("\033*b%dW", pcks * 8);
          for (byte s : line)
          {
            out.write(s);
          }
          for (int k = 0; k < 8 - (len % 8); k++)
          {
            out.write((byte) 128);
          }
          leftToRight = !leftToRight;
        }
      }

    }
    out.printf("\033*rC");       // end raster
    return result.toByteArray();
  }

  private byte[] generateVectorPCL(LaserJob job, VectorPart vp) throws UnsupportedEncodingException
  {

    ByteArrayOutputStream result = new ByteArrayOutputStream();
    PrintStream out = new PrintStream(result, true, "US-ASCII");

    out.printf("\033*r0F");
    out.printf("\033*r%dT", vp == null ? 500 : vp.getMaxY());// if not dummy, then job.getHeight());
    out.printf("\033*r%dS", vp == null ? 500 : vp.getMaxX());// if not dummy then job.getWidth());
    out.printf("\033*r1A");
    out.printf("\033*rC");
    out.printf("\033%%1B");// Start HLGL
    out.printf("IN;PU0,0;");

    if (vp != null)
    {
      Integer currentPower = null;
      Integer currentSpeed = null;
      Integer currentFrequency = null;
      Float currentFocus = null;
      int sx = job.getStartX();
      int sy = job.getStartY();
      VectorCommand.CmdType lastType = null;
      for (VectorCommand cmd : vp.getCommandList())
      {
        if (lastType != null && lastType == VectorCommand.CmdType.LINETO && cmd.getType() != VectorCommand.CmdType.LINETO)
        {
          out.print(";");
        }
        switch (cmd.getType())
        {
          case SETPROPERTY:
          {
            PowerSpeedFocusFrequencyProperty p = (PowerSpeedFocusFrequencyProperty) cmd.getProperty();
            if (currentFocus == null || !currentFocus.equals(p.getFocus()))
            {
              out.printf("WF%d;", mm2focus(p.getFocus()));
              currentFocus = p.getFocus();
            }
            if (currentFrequency == null || !currentFrequency.equals(p.getFrequency()))
            {
              out.printf("XR%04d;", p.getFrequency());
              currentFrequency = p.getFrequency();
            }
            if (currentPower == null || !currentPower.equals(p.getPower()))
            {
              out.printf("YP%03d;", p.getPower());
              currentPower = p.getPower();
            }
            if (currentSpeed == null || !currentSpeed.equals(p.getSpeed()))
            {
              out.printf("ZS%03d;", p.getSpeed());
              currentSpeed = p.getSpeed();
            }
            break;
          }
          case MOVETO:
          {
            out.printf("PU%d,%d;", cmd.getX() - sx, cmd.getY() - sy);
            break;
          }
          case LINETO:
          {
            if (lastType == null || lastType != VectorCommand.CmdType.LINETO)
            {
              out.printf("PD%d,%d", cmd.getX() - sx, cmd.getY() - sy);
            }
            else
            {
              out.printf(",%d,%d", cmd.getX() - sx, cmd.getY() - sy);
            }
            break;
          }
        }
        lastType = cmd.getType();
      }
    }
    //Reset Focus to 0
    out.printf("WF%d;", 0);
    return result.toByteArray();
  }

  private byte[] generatePjlData(LaserJob job) throws UnsupportedEncodingException, IOException
  {
    /* Generate complete PJL Job */
    ByteArrayOutputStream pjlJob = new ByteArrayOutputStream();
    PrintStream wrt = new PrintStream(pjlJob, true, "US-ASCII");

    wrt.write(generatePjlHeader(job));
    for (JobPart p : job.getParts())
    {
      if (p instanceof VectorPart)
      {
        wrt.write(generateVectorPCL(job, (VectorPart) p));
      }
      else if (p instanceof RasterPart)
      {
        wrt.write(generateRasterPCL(job, (RasterPart) p));
      }
      else if (p instanceof Raster3dPart)
      {
        wrt.write(generateRaster3dPCL(job, (Raster3dPart) p));
      }
    }
    wrt.write(generatePjlFooter());
    /* Pad out the remainder of the file with 0 characters. */
    for (int i = 0; i < 4096; i++)
    {
      wrt.append((char) 0);
    }
    wrt.flush();
    return pjlJob.toByteArray();
  }

  public int getPort()
  {
    return this.port;
  }

  public void setPort(int Port)
  {
    this.port = Port;
  }

  @Override
  public String getSettingValue(String attribute)
  {
    if ("Hostname".equals(attribute))
    {
      return this.getHostname();
    }
    else if ("AutoFocus".equals(attribute))
    {
      return "" + this.isAutoFocus();
    }
    else if ("Port".equals(attribute))
    {
      return "" + this.getPort();
    }
    else if ("BedWidth".equals(attribute))
    {
      return "" + this.getBedWidth();
    }
    else if ("BedHeight".equals(attribute))
    {
      return "" + this.getBedHeight();
    }
    return null;
  }
  protected double bedWidth = 600;

  /**
   * Get the value of bedWidth
   *
   * @return the value of bedWidth
   */
  @Override
  public double getBedWidth()
  {
    return bedWidth;
  }

  /**
   * Set the value of bedWidth
   *
   * @param bedWidth new value of bedWidth
   */
  public void setBedWidth(double bedWidth)
  {
    this.bedWidth = bedWidth;
  }
  protected double bedHeight = 300;

  /**
   * Get the value of bedHeight
   *
   * @return the value of bedHeight
   */
  @Override
  public double getBedHeight()
  {
    return bedHeight;
  }

  /**
   * Set the value of bedHeight
   *
   * @param bedHeight new value of bedHeight
   */
  public void setBedHeight(double bedHeight)
  {
    this.bedHeight = bedHeight;
  }

  @Override
  public void setSettingValue(String attribute, String value)
  {
    if ("Hostname".equals(attribute))
    {
      this.setHostname(value);
    }
    else if ("AutoFocus".endsWith(attribute))
    {
      this.setAutoFocus(Boolean.parseBoolean(value));
    }
    else if ("Port".equals(attribute))
    {
      this.setPort(Integer.parseInt(value));
    }
    else if ("BedWidth".equals(attribute))
    {
      this.setBedWidth(Double.parseDouble(value));
    }
    else if ("BedHeight".equals(attribute))
    {
      this.setBedHeight(Double.parseDouble(value));
    }
  }
  private String[] attributes = new String[]
  {
    "Hostname", "Port", "BedWidth", "BedHeight", "AutoFocus"
  };

  @Override
  public List<String> getSettingAttributes()
  {
    return Arrays.asList(attributes);
  }

  @Override
  public boolean canEstimateJobDuration()
  {
    return true;
  }
  
  @Override
  public int estimateJobDuration(LaserJob job)
  {
    double VECTOR_MOVESPEED_X = 20000d / 4.5;
    double VECTOR_MOVESPEED_Y = 10000d / 2.5;
    double VECTOR_LINESPEED = 20000d / 36.8;
    double RASTER_LINEOFFSET = 0.08d;
    double RASTER_LINESPEED = 100000d / ((268d / 50) - RASTER_LINEOFFSET);
    //TODO: The Raster3d values are not tested yet, theyre just copies
    double RASTER3D_LINEOFFSET = 0.08;
    double RASTER3D_LINESPEED = 100000d / ((268d / 50) - RASTER3D_LINEOFFSET);

    //Holds the current Laser Head position in Pixels
    Point p = new Point(0, 0);

    double result = 0;//usual offset
    for (JobPart jp : job.getParts())
    {
      if (jp instanceof RasterPart)
      {
        RasterPart rp = (RasterPart) jp;
        for (int i = 0; i < rp.getRasterCount(); i++)
        {//Time to move to Start Position
          Point sp = rp.getRasterStart(i);
          result += Math.max((double) (p.x - sp.x) / VECTOR_MOVESPEED_X,
            (double) (p.y - sp.y) / VECTOR_MOVESPEED_Y);
          double linespeed = ((double) RASTER_LINESPEED * ((PowerSpeedFocusProperty) rp.getLaserProperty(i)).getSpeed()) / 100;
          BlackWhiteRaster bwr = rp.getImages()[i];
          for (int y = 0; y < bwr.getHeight(); y++)
          {//Find any black point
            boolean lineEmpty = true;
            for (int x = 0; x < bwr.getWidth(); x++)
            {
              if (bwr.isBlack(x, y))
              {
                lineEmpty = false;
                break;
              }
            }
            if (!lineEmpty)
            {
              int w = bwr.getWidth();
              result += (double) RASTER_LINEOFFSET + (double) w / linespeed;
              p.x = sp.y % 2 == 0 ? sp.x + w : sp.x;
              p.y = sp.y + y;
            }
            else
            {
              result += RASTER_LINEOFFSET;
            }
          }
        }
      }
      if (jp instanceof Raster3dPart)
      {
        Raster3dPart rp = (Raster3dPart) jp;
        for (int i = 0; i < rp.getRasterCount(); i++)
        {//Time to move to Start Position
          Point sp = rp.getRasterStart(i);
          result += Math.max((double) (p.x - sp.x) / VECTOR_MOVESPEED_X,
            (double) (p.y - sp.y) / VECTOR_MOVESPEED_Y);
          double linespeed = ((double) RASTER3D_LINESPEED * ((PowerSpeedFocusProperty) rp.getLaserProperty(i)).getSpeed()) / 100;
          GreyscaleRaster gsr = rp.getImages()[i];
          for (int y = 0; y < gsr.getHeight(); y++)
          {//Check if
            boolean lineEmpty = true;
            for (int x = 0; x < gsr.getWidth(); x++)
            {
              if (gsr.getGreyScale(x, y) != 0)
              {
                lineEmpty = false;
                break;
              }
            }
            if (!lineEmpty)
            {
              int w = gsr.getWidth();
              result += (double) RASTER3D_LINEOFFSET + (double) w / linespeed;
              p.x = sp.y % 2 == 0 ? sp.x + w : sp.x;
              p.y = sp.y + y;
            }
          }
        }
      }
      if (jp instanceof VectorPart)
      {
        double speed = VECTOR_LINESPEED;
        VectorPart vp = (VectorPart) jp;
        for (VectorCommand cmd : vp.getCommandList())
        {
          switch (cmd.getType())
          {
            case SETPROPERTY:
            {
              speed = VECTOR_LINESPEED * ((PowerSpeedFocusFrequencyProperty) cmd.getProperty()).getSpeed() / 100;
              break;
            }
            case MOVETO:
              result += Math.max((double) (p.x - cmd.getX()) / VECTOR_MOVESPEED_X,
                (double) (p.y - cmd.getY()) / VECTOR_MOVESPEED_Y);
              p = new Point(cmd.getX(), cmd.getY());
              break;
            case LINETO:
              double dist = distance(cmd.getX(), cmd.getY(), p);
              p = new Point(cmd.getX(), cmd.getY());
              result += dist / speed;
              break;
          }
        }
      }
    }
    return (int) result;
  }

  private double distance(int x, int y, Point p)
  {
    return Math.sqrt(Math.pow(p.x - x, 2) + Math.pow(p.y - y, 2));
  }
  
}